Skip to content

Commit

Permalink
Refactoring of aggregates [#72]. Rewritten all Aggregate RSpecs, chan…
Browse files Browse the repository at this point in the history
…ged DSL and renamed classes.

Also fixed a cascade_delete bug.
  • Loading branch information
andreas committed Nov 14, 2009
1 parent 21f4009 commit d18fc48
Show file tree
Hide file tree
Showing 42 changed files with 863 additions and 662 deletions.
21 changes: 10 additions & 11 deletions lib/neo4j/extensions/aggregate.rb
@@ -1,13 +1,12 @@
require 'set'
require 'neo4j/extensions/aggregate/aggregator'
require 'neo4j/extensions/aggregate/group_enum'
require 'neo4j/extensions/aggregate/group_node'
require 'neo4j/extensions/aggregate/property_enum'
require 'neo4j/extensions/aggregate/aggregate_node_mixin'
require 'neo4j/extensions/aggregate/aggregate_node'
require 'neo4j/extensions/aggregate/ext/node_mixin'

require 'neo4j/extensions/aggregate/aggregator_each'
require 'neo4j/extensions/aggregate/group_each_node'
require 'neo4j/extensions/aggregate/aggregate_each_node_mixin'
require 'neo4j/extensions/aggregate/aggregate_each_node'
require 'neo4j/extensions/aggregate/node_aggregate_mixin'
require 'neo4j/extensions/aggregate/props_aggregate_mixin'
require 'neo4j/extensions/aggregate/node_aggregate'
require 'neo4j/extensions/aggregate/props_aggregate'
require 'neo4j/extensions/aggregate/aggregate_enum'
require 'neo4j/extensions/aggregate/node_aggregator'
require 'neo4j/extensions/aggregate/node_group'
require 'neo4j/extensions/aggregate/prop_group'
require 'neo4j/extensions/aggregate/property_enum'
require 'neo4j/extensions/aggregate/props_aggregator'
6 changes: 0 additions & 6 deletions lib/neo4j/extensions/aggregate/aggregate_each_node.rb

This file was deleted.

@@ -1,23 +1,27 @@
module Neo4j::Aggregate
# Used for an enumerable result of aggregates
# Used for an enumerable result of aggregates
# See Neo4j::NodeMixin#aggregates
#
# :api: private
class GroupEnum #:nodoc:
class AggregateEnum #:nodoc:
include Enumerable

def initialize(node)
@node = node
end

def empty?
each {true}.nil?
end

def each
# if node is an aggregate group then we should look for parent aggregates
if (@node.property?(:aggregate_group))
@node.relationships.incoming.nodes.each do |parent_group|
next unless parent_group.property?(:aggregate_size)
# if it has the property aggregate_group then it is a group node
if (parent_group.property?(:aggregate_group))
GroupEnum.new(parent_group).each {|agg| yield agg}
AggregateEnum.new(parent_group).each {|agg| yield agg}
else
# aggregate found
yield parent_group
Expand All @@ -27,7 +31,7 @@ def each
# the given node (@node) is not a group, we guess it is an leaf in an aggregate
# get all the groups that this leaf belongs to and then those groups aggregate nodes
@node.relationships.incoming(:aggregate).nodes.each do |group|
GroupEnum.new(group ).each {|agg| yield agg}
AggregateEnum.new(group ).each {|agg| yield agg}
end
end
end
Expand Down
9 changes: 0 additions & 9 deletions lib/neo4j/extensions/aggregate/aggregate_node.rb

This file was deleted.

11 changes: 4 additions & 7 deletions lib/neo4j/extensions/aggregate/ext/node_mixin.rb
Expand Up @@ -11,22 +11,19 @@ module NodeMixin
#
# class MyNode
# include Neo4j::NodeMixin
# include Neo4j::AggregateNodeMixin
# include Neo4j::NodeAggregateMixin
# end
#
# agg1 = MyNode
# agg1.aggregate(:colours).group_by(:colour)
# agg1.aggregate([node1,node2]).group_by(:colour)
#
# agg2 = MyNode
# agg2.aggregate(:age).group_by(:age)
#
# agg1 << node1
# agg2 << node1
# agg2.aggregate([node1,node2]).group_by(:age)
#
# node1.aggregates.to_a # => [agg1, agg2]
#
def aggregates
Neo4j::Aggregate::GroupEnum.new(self)
Neo4j::Aggregate::AggregateEnum.new(self)
end

# Returns an enumeration of groups that this nodes belongs to.
Expand Down
8 changes: 8 additions & 0 deletions lib/neo4j/extensions/aggregate/node_aggregate.rb
@@ -0,0 +1,8 @@
module Neo4j::Aggregate

class NodeAggregate
include Neo4j::NodeMixin
include Neo4j::Aggregate::NodeAggregateMixin
end

end
Expand Up @@ -2,27 +2,32 @@ module Neo4j::Aggregate



# Enables aggregation of an enumeration of nodes into groups.
# Each group is a neo4j node which contains aggregated properties of the underlying nodes in that group.
# Enables aggregation of nodes into groups.
# An aggregation is a node which in contains group nodes.
# A group node aggregates the properties of the nodes that belongs to its group.
#
# Notice that the AggregateNodeMixin#aggregate method takes an Ruby Enumeration of neo4j nodes.
# That means that you can use for example the output from the Neo4j::NodeMixin#traverse as input to the aggregate method, or even
# There are two ways of creating an aggregate.
# * Providing an enumeration of nodes.
# * Register a Node class. All nodes of this class will (can) be part of the aggregate.
#
# One example of usage of providing an enumeration of nodes is by taking the output from
# the Neo4j::NodeMixin#traverse as input to the aggregate method, or even
# create aggregates over aggregates.
#
# This mixin includes the Enumerable mixin.
#
# There is also a different aggregation which aggregates on properties
# instead of nodes - Neo4j::Aggregate::PropsAggregateMixin
#
# ==== Example - group by one property
#
# Let say we have nodes with properties :colour and we want to group them by colour:
#
# a = AggregateNode.new
#
# a.aggregate(nodes).group_by(:colour)
#
# The following node structure will be created:
#
# [node a]--<relationship type red|green|blue...>--*>[node groups]--<relationship type aggregate>--*>[node nodes]
# a.aggregate(nodes).group_by(:colour).execute
#
# The execute method is only needed when providing nodes (instead of a NodeClass) for the aggregate method.
# Print all three groups, one for each colour
#
# a.each{|n| puts n[:colour]}
Expand All @@ -38,7 +43,7 @@ module Neo4j::Aggregate
#
# Get an enumeration of names of people having favorite colour 'red'
#
# a.[:red][:name].to_a => ['bertil', 'adam', 'adam']
# a[:red][:name].to_a => ['bertil', 'adam', 'adam']
#
# ==== Example - group by a property value which is transformed
#
Expand Down Expand Up @@ -95,13 +100,13 @@ module Neo4j::Aggregate
#
# One example where this is needed is for having a tree structure of nodes with latitude and longitude grouped by a 'zoom' factor
#
# create an aggrgeation of groups where members have the same latitude longitude integer values (to_i)
# create an aggregation of groups where members have the same latitude longitude integer values (to_i)
# reg1 = agg_root.aggregate().group_by(:latitude, :longitude).map_value{|lat, lng| "#{(lat*1000).to_i}_#{(lng*1000).to_i}"}
#
# create another aggregation of groups where members have the same latitude longitude 1/10 value
# reg2 = agg_root.aggregate(reg1).group_by(:latitude, :longitude).map_value{|lat, lng| "#{(lat*100).to_i}_#{(lng*100).to_i" }
#
# Notice how the second aggreate uses the first aggregate (reg1). This will create the following structure with
# Notice how the second aggregate uses the first aggregate (reg1). This will create the following structure with
# * node n1 - (latitude 42.1234 and longitude 12.1234) and
# * node n2 (latitude 42.1299 and longitude 12.1298)
# * node n3 (latitude 42.1333 and longitude 12.1298)
Expand All @@ -117,28 +122,12 @@ module Neo4j::Aggregate
# When the nodes n1,n2,n3 are added to the agg_root, e.g:
# agg_root << n1 << n2 << n3
#
# ==== Example - aggregating over another aggregation
#
# a = AggregateNode.new
# a.aggregate.group_by(:colour)
# a << node1 << node2
#
# b = AggregateNode.new
# b.aggregate.group_by(:age)
# node3[:colour] = 'green'; node3[:age] = 10
# node4[:colour] = 'red'; node3[:age] = 11
#
# b << node3 << node4
#
# a << b
#
# a['green'][10] #=>[node3]
#
#
# ==== Example - Add and remove nodes by events
#
# We want to both create and delete nodes and the aggregates should be updated automatically
# This is done by registering the aggregate dsl method as an event listener
# This is done by providing a NodeClass for the aggregate method.
# (it registering the aggregate dsl method as an event listener
#
# Here is an example that update the aggregate a on all nodes of type MyNode
# a = AggregateNode.new
Expand Down Expand Up @@ -231,13 +220,24 @@ module Neo4j::Aggregate
# a[:rev] => ["good", "good", "bad"]
# a[:rev]["good"] => 2
# a[:rev]["bad"] => 1
module AggregateNodeMixin
module NodeAggregateMixin
include Neo4j::NodeMixin
property :aggregate_size # number of groups this aggregate contains
include Enumerable


# The number of groups that this aggregate contains
def aggregate_size
internal_node.set_property("aggregate_size", 0) unless internal_node.has_property("aggregate_size")
self[:aggregate_size]
end


# Internal method - set the number of groups that this node contains
# We can then use this property instead of traversing and counting each node in order to find out how many groups there are.
def aggregate_size=(value) # :nodoc:
self[:aggregate_size] = value
end

# Creates aggregated nodes by grouping nodes by one or more property values.
# Raises an exception if the aggregation already exists.
#
Expand All @@ -255,8 +255,7 @@ module AggregateNodeMixin
# :api: public
def aggregate(nodes_or_filter=nil)
# setting a property here using neo4j.rb might trigger events which we do not want
internal_node.set_property("aggregate_size", 0) unless internal_node.has_property("aggregate_size")
@aggregator = Aggregator.new(self, nodes_or_filter)
@aggregator = NodeAggregator.new(self, nodes_or_filter)
end

# Appends one or a whole enumeration of nodes to the existing aggregation.
Expand Down Expand Up @@ -303,7 +302,7 @@ def include_node?(node)
# :api: public
def group_node(key)
@aggregator.execute if @aggregator
relationships.outgoing(key).nodes.find{|n| n.kind_of? AggregateGroupNode}
relationships.outgoing(key).nodes.find{|n| n.kind_of? NodeGroup}
end


Expand All @@ -325,7 +324,7 @@ def get_property(key)

def each
@aggregator.execute if @aggregator
relationships.outgoing.nodes.each {|n| yield n if n.kind_of? AggregateGroupNode}
relationships.outgoing.nodes.each {|n| yield n if n.kind_of? NodeGroup}
end

end
Expand Down
Expand Up @@ -2,7 +2,7 @@ module Neo4j::Aggregate
# Used to create a DSL describing how to aggregate an enumeration of nodes
#
# :api: public
class Aggregator
class NodeAggregator
attr_accessor :root_dsl

def initialize(root_node, dsl_nodes_or_filter)
Expand All @@ -12,7 +12,7 @@ def initialize(root_node, dsl_nodes_or_filter)
if dsl_nodes_or_filter.kind_of?(self.class)
# we are chaining aggregates
@child_dsl = dsl_nodes_or_filter
@child_dsl.root_dsl = self # the child has a pointer to the parent
@child_dsl.root_dsl = self # the child has a pointer to the parent
elsif dsl_nodes_or_filter.kind_of?(Enumerable)
# we are aggregating an enumerable set of nodes
@nodes = dsl_nodes_or_filter
Expand Down Expand Up @@ -62,16 +62,15 @@ def on_property_changed(node, prop_key, old_value, new_value) # :nodoc:
# :api: private
def on_node_deleted(node) # :nodoc:
return if node.class != @filter
member_of = node.relationships.incoming(:aggregate).filter{start_node.property? :aggregate_size}.to_a
return if member_of.empty?
group_node = member_of[0].start_node
group_node.aggregate_size -= 1

# should we delete the whole group ?
delete_group(group_node) if (group_node.aggregate_size == 0)
node.relationships.incoming(:aggregate).filter{start_node.property? :aggregate_size}.each do |group_rel|
group_node = group_rel.start_node
group_node.aggregate_size -= 1
# should we delete the whole group ?
delete_group(group_node) if (group_node.aggregate_size == 0)
end
end

def delete_group(group_node) # :nodoc:
def delete_group(group_node) # :nodoc:
# get parent aggregates and decrease the aggregate size
group_node.relationships.incoming.nodes.each do |parent_group|
next unless parent_group.respond_to? :aggregate_size
Expand All @@ -82,7 +81,7 @@ def delete_group(group_node) # :nodoc:
end


def on_prop_deleted(node, curr_node_values, old_node_values) # :nodoc:
def on_prop_deleted(node, curr_node_values, old_node_values) # :nodoc:
old_group_keys = group_key_of(old_node_values)
new_group_keys = group_key_of(curr_node_values)

Expand All @@ -103,7 +102,7 @@ def on_prop_deleted(node, curr_node_values, old_node_values) # :nodoc:

end

def on_prop_added(node, curr_node_values, old_node_values) # :nodoc:
def on_prop_added(node, curr_node_values, old_node_values) # :nodoc:
old_group_keys = group_key_of(old_node_values)
new_group_keys = group_key_of(curr_node_values)

Expand Down Expand Up @@ -177,18 +176,18 @@ def create_groups(parent, node)
# :api: private
def create_group_for_key(parent, node, key)
# find a group node for the given key
group_node = parent.relationships.outgoing(key).nodes.find{|n| n.kind_of? AggregateGroupNode}
group_node = parent.relationships.outgoing(key).nodes.find{|n| n.kind_of? NodeGroup}

# if no group key is found create a new one
group_node ||= create_group_node(parent, 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)
@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
rel_type = node.kind_of?(NodeGroup)? key : :aggregate
rel = group_node.relationships.outgoing(rel_type) << node
rel[:aggregate_group] = key
# increase the size counter on this group
Expand All @@ -198,7 +197,7 @@ def create_group_for_key(parent, node, key)

# :api: private
def create_group_node(parent, key)
new_node = AggregateGroupNode.create(key)
new_node = NodeGroup.create(key)
rel = parent.relationships.outgoing(key) << new_node
parent.aggregate_size += 1 # another group was created
rel[:aggregate_group] = key
Expand Down
Expand Up @@ -5,14 +5,14 @@ module Neo4j::Aggregate
# Overrides [] and []= properties, so that we can access aggregated properties or relationships.
#
# :api: private
class AggregateGroupNode #:nodoc:
class NodeGroup #:nodoc:
include Neo4j::NodeMixin
include Enumerable

property :aggregate_group, :aggregate_size

def self.create(aggregate_group)
new_node = AggregateGroupNode.new
new_node = NodeGroup.new
new_node.aggregate_group = aggregate_group.kind_of?(Symbol)? aggregate_group.to_s : aggregate_group
new_node.aggregate_size = 0
new_node
Expand Down

0 comments on commit d18fc48

Please sign in to comment.