neo4j 1.0.0.beta.17 → 1.0.0.beta.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,156 @@
1
+ module Neo4j
2
+ module Index
3
+ # == LuceneQuery
4
+ #
5
+ # This object is returned when you call the #find method on the Node, Relationship.
6
+ # The actual query is not executed until the first item is requested.
7
+ #
8
+ # You can perform a query in many different ways:
9
+ #
10
+ # ==== By Hash
11
+ #
12
+ # Example:
13
+ # Person.find(:name => 'foo', :age => 3)
14
+ #
15
+ # ==== By Range
16
+ #
17
+ # Example:
18
+ # Person.find(:age).between(15,35)
19
+ #
20
+ # ==== By Lucene Query Syntax
21
+ #
22
+ # Example
23
+ # Car.find('wheels:"4" AND colour: "blue")
24
+ #
25
+ # For more information about the syntax see http://lucene.apache.org/java/3_0_2/queryparsersyntax.html
26
+ #
27
+ # ==== By Compound Queries
28
+ #
29
+ # You can combine several queries by <tt>AND</tt>ing those together.
30
+ #
31
+ # Example:
32
+ # Vehicle.find(:weight).between(5.0, 100000.0).and(:name).between('a', 'd')
33
+ #
34
+ # === See Also
35
+ # * Neo4j::Index::Indexer#index
36
+ # * Neo4j::Index::Indexer#find - which returns an LuceneQuery
37
+ #
38
+ class LuceneQuery
39
+ include Enumerable
40
+ attr_accessor :left_and_query, :left_or_query
41
+
42
+ def initialize(index, decl_props, query)
43
+ @index = index
44
+ @query = query
45
+ @decl_props = decl_props
46
+ end
47
+
48
+ def each
49
+ hits.each { |n| yield n.wrapper }
50
+ end
51
+
52
+ def close
53
+ @hits.close if @hits
54
+ end
55
+
56
+ def empty?
57
+ hits.size == 0
58
+ end
59
+
60
+ def [](index)
61
+ each_with_index {|node,i| break node if index == i}
62
+ end
63
+
64
+ def size
65
+ hits.size
66
+ end
67
+
68
+ def hits
69
+ @hits ||= perform_query
70
+ end
71
+
72
+ def between(lower, upper)
73
+ raise "Expected a symbol. Syntax for range queries example: index(:weight).between(a,b)" unless Symbol === @query
74
+ raise "Can't only do range queries on Neo4j::NodeMixin, Neo4j::Model, Neo4j::RelationshipMixin" unless @decl_props
75
+ type = @decl_props[@query] && @decl_props[@query][:type]
76
+ raise "Missing type declaration of property #{@query}. E.g. property :#{@query}, :type => Float; index :#{@query}" unless type
77
+ if type != String
78
+ raise "find(#{@query}).between(#{lower}, #{upper}) to allowed since #{lower} is not a Float or Fixnum" if lower === Float or lower === Fixnum
79
+ raise "find(#{@query}).between(#{lower}, #{upper}) to allowed since #{upper} is not a Float or Fixnum" if upper === Float or upper === Fixnum
80
+ @query = org.apache.lucene.search.NumericRangeQuery.new_double_range(@query.to_s, lower, upper, false, false)
81
+ else
82
+ raise "find(#{@query}).between(#{lower}, #{upper}) to allowed since #{lower} is not a String" if lower === String
83
+ raise "find(#{@query}).between(#{lower}, #{upper}) to allowed since #{upper} is not a String" if upper === String
84
+ @query = org.apache.lucene.search.TermRangeQuery.new(@query.to_s, lower, upper, false, false)
85
+ end
86
+ self
87
+ end
88
+
89
+ def and(query2)
90
+ new_query = LuceneQuery.new(@index, @decl_props, query2)
91
+ new_query.left_and_query = self
92
+ new_query
93
+ end
94
+
95
+ def desc(*fields)
96
+ @order = fields.inject(@order || {}) { |memo, field| memo[field] = true; memo }
97
+ self
98
+ end
99
+
100
+ def asc(*fields)
101
+ @order = fields.inject(@order || {}) { |memo, field| memo[field] = false; memo }
102
+ self
103
+ end
104
+
105
+ def build_and_query(query)
106
+ left_query = @left_and_query.build_query
107
+ and_query = org.apache.lucene.search.BooleanQuery.new
108
+ and_query.add(left_query, org.apache.lucene.search.BooleanClause::Occur::MUST)
109
+ and_query.add(query, org.apache.lucene.search.BooleanClause::Occur::MUST)
110
+ and_query
111
+ end
112
+
113
+ def build_sort_query(query)
114
+ java_sort_fields = @order.keys.inject([]) do |memo, field|
115
+ decl_type = @decl_props && @decl_props[field] && @decl_props[field][:type]
116
+ type = case
117
+ when Float == decl_type
118
+ org.apache.lucene.search.SortField::DOUBLE
119
+ when Fixnum == decl_type
120
+ org.apache.lucene.search.SortField::LONG
121
+ else
122
+ org.apache.lucene.search.SortField::STRING
123
+ end
124
+ memo << org.apache.lucene.search.SortField.new(field.to_s, type, @order[field])
125
+ end
126
+ sort = org.apache.lucene.search.Sort.new(*java_sort_fields)
127
+ org.neo4j.index.impl.lucene.QueryContext.new(query).sort(sort)
128
+ end
129
+
130
+ def build_hash_query(query)
131
+ and_query = org.apache.lucene.search.BooleanQuery.new
132
+
133
+ query.each_pair do |key, value|
134
+ raise "Only String values valid in find(hash) got :#{key} => #{value} which is not a String" if !value.is_a?(String) && @decl_props[key] && @decl_props[key][:type] != String
135
+ term = org.apache.lucene.index.Term.new(key.to_s, value.to_s)
136
+ term_query = org.apache.lucene.search.TermQuery.new(term)
137
+ and_query.add(term_query, org.apache.lucene.search.BooleanClause::Occur::MUST)
138
+ end
139
+ and_query
140
+ end
141
+
142
+ def build_query
143
+ query = @query
144
+ query = build_hash_query(query) if Hash === query
145
+ query = build_and_query(query) if @left_and_query
146
+ query = build_sort_query(query) if @order
147
+ query
148
+ end
149
+
150
+ def perform_query
151
+ @index.query(build_query)
152
+ end
153
+ end
154
+ end
155
+ end
156
+
data/lib/neo4j/load.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  module Neo4j
2
2
 
3
- # Module used to load both Nodes and Relationship from the database
3
+ # === Mixin responsible for loading Ruby wrappers for Neo4j Nodes and Relationship.
4
+ #
4
5
  module Load
5
6
  def wrapper(node) # :nodoc:
6
7
  return node unless node.property?(:_classname)
@@ -11,7 +12,7 @@ module Neo4j
11
12
  class_name.split("::").inject(Kernel) {|container, name| container.const_get(name.to_s) }
12
13
  end
13
14
 
14
- # Checks if the given node or node id exists in the database.
15
+ # Checks if the given entity (node/relationship) or entity id (#neo_id) exists in the database.
15
16
  def exist?(node_or_node_id, db = Neo4j.started_db)
16
17
  id = node_or_node_id.kind_of?(Fixnum) ? node_or_node_id : node_or_node_id.id
17
18
  _load(id, db) != nil
@@ -6,7 +6,23 @@ module Neo4j::Mapping
6
6
  # The generated accessor is a simple wrapper around the #[] and
7
7
  # #[]= operators.
8
8
  #
9
+ # ==== Types
9
10
  # If a property is set to nil the property will be removed.
11
+ # A property can be of any primitive type (Boolean, String, Fixnum, Float) and does not
12
+ # even have to be the same.
13
+ #
14
+ # Example:
15
+ # class Foo
16
+ # include Neo4j::NodeMixin
17
+ # property :age
18
+ # end
19
+ #
20
+ # Example:
21
+ # foo = Foo.new
22
+ # foo.age = "hej" # first set it to string
23
+ # foo.age = 42 # change it to a Fixnum
24
+ #
25
+ # However, you can specify an type for the index, see Neo4j::Index::Indexer#index
10
26
  #
11
27
  # ==== Example
12
28
  # class Baaz; end
@@ -6,6 +6,7 @@ module Neo4j::Mapping
6
6
 
7
7
  # Specifies a relationship between two node classes.
8
8
  # Generates assignment and accessor methods for the given relationship.
9
+ # Both incoming and outgoing relationships can be declared, see Neo4j::Mapping::DeclRelationshipDsl
9
10
  #
10
11
  # ==== Example
11
12
  #
@@ -43,6 +44,7 @@ module Neo4j::Mapping
43
44
  # Specifies a relationship between two node classes.
44
45
  # Generates assignment and accessor methods for the given relationship
45
46
  # Old relationship is deleted when a new relationship is assigned.
47
+ # Both incoming and outgoing relationships can be declared, see Neo4j::Mapping::DeclRelationshipDsl
46
48
  #
47
49
  # ==== Example
48
50
  #
@@ -58,7 +60,7 @@ module Neo4j::Mapping
58
60
  #
59
61
  # ==== Returns
60
62
  #
61
- # Neo4j::Relationships::DeclRelationshipDsl
63
+ # Neo4j::Mapping::DeclRelationshipDsl
62
64
  #
63
65
  def has_one(rel_type, params = {})
64
66
  clazz = self
@@ -1,5 +1,6 @@
1
1
  module Neo4j::Mapping
2
2
  module ClassMethods
3
+ # Used to hold information about which relationships and properties has been declared.
3
4
  module Root
4
5
  #attr_reader :_decl_rels, :_decl_props
5
6
 
@@ -13,11 +14,13 @@ module Neo4j::Mapping
13
14
  end
14
15
 
15
16
 
17
+ # a hash of all relationships which has been declared with a has_n or has_one using Neo4j::Mapping::ClassMethods::Relationship
16
18
  def _decl_rels
17
19
  @@_all_decl_rels[self] ||= {}
18
20
  @_decl_props = @@_all_decl_rels[self]
19
21
  end
20
22
 
23
+ # a hash of all properties which has been declared with <tt>property</tt> using the Neo4j::Mapping::ClassMethods::Property
21
24
  def _decl_props
22
25
  @@_all_decl_props[self] ||= {}
23
26
  @_decl_props = @@_all_decl_props[self]
@@ -1,39 +1,43 @@
1
1
  module Neo4j::Mapping
2
2
  module ClassMethods
3
+ # Holds all defined rules and trigger them when an event is received.
4
+ #
5
+ # See Rule
6
+ #
3
7
  class Rules
4
8
  class << self
5
9
  def add(clazz, field, props, &block)
6
- clazz = clazz.to_s
7
- @rules ||= {}
10
+ clazz = clazz.to_s
11
+ @rules ||= {}
8
12
  # was there no ruls for this class AND is neo4j running ?
9
13
  if !@rules.include?(clazz) && Neo4j.running?
10
14
  # maybe Neo4j was started first and the rules was added later. Create rule nodes now
11
15
  create_rule_node_for(clazz)
12
16
  end
13
- @rules[clazz] ||= {}
14
- filter = block.nil? ? Proc.new { |*| true } : block
15
- @rules[clazz][field] = filter
16
- @triggers ||= {}
17
- @triggers[clazz] ||= {}
18
- trigger = props[:trigger].nil? ? [] : props[:trigger]
17
+ @rules[clazz] ||= {}
18
+ filter = block.nil? ? Proc.new { |*| true } : block
19
+ @rules[clazz][field] = filter
20
+ @triggers ||= {}
21
+ @triggers[clazz] ||= {}
22
+ trigger = props[:trigger].nil? ? [] : props[:trigger]
19
23
  @triggers[clazz][field] = trigger.respond_to?(:each) ? trigger : [trigger]
20
24
  end
21
-
22
- def inherit(parent_class, subclass)
23
- # copy all the rules
24
- @rules[parent_class.to_s].each_pair do |field, filter|
25
- subclass.rule field, &filter
26
- end if @rules[parent_class.to_s]
27
- end
25
+
26
+ def inherit(parent_class, subclass)
27
+ # copy all the rules
28
+ @rules[parent_class.to_s].each_pair do |field, filter|
29
+ subclass.rule field, &filter
30
+ end if @rules[parent_class.to_s]
31
+ end
28
32
 
29
33
  def trigger_other_rules(node)
30
- clazz = node[:_classname]
31
- @rules[clazz].keys.each do |field|
32
- rel_types = @triggers[clazz][field]
33
- rel_types.each do |rel_type|
34
- node.incoming(rel_type).each { |n| n.trigger_rules }
35
- end
36
- end
34
+ clazz = node[:_classname]
35
+ @rules[clazz].keys.each do |field|
36
+ rel_types = @triggers[clazz][field]
37
+ rel_types.each do |rel_type|
38
+ node.incoming(rel_type).each { |n| n.trigger_rules }
39
+ end
40
+ end
37
41
  end
38
42
 
39
43
  def fields_for(clazz)
@@ -70,17 +74,17 @@ module Neo4j::Mapping
70
74
  end
71
75
 
72
76
  def rule_for(clazz)
73
- if Neo4j.ref_node.rel?(clazz)
74
- Neo4j.ref_node._rel(:outgoing, clazz)._end_node
75
- else
76
- # this should be called if the rule node gets deleted
77
- create_rule_node_for(clazz)
78
- end
77
+ if Neo4j.ref_node.rel?(clazz)
78
+ Neo4j.ref_node._rel(:outgoing, clazz)._end_node
79
+ else
80
+ # this should be called if the rule node gets deleted
81
+ create_rule_node_for(clazz)
82
+ end
79
83
  end
80
84
 
81
85
 
82
86
  def on_relationship_created(rel, *)
83
- trigger_start_node = trigger?(rel._start_node)
87
+ trigger_start_node = trigger?(rel._start_node)
84
88
  trigger_end_node = trigger?(rel._end_node)
85
89
  # end or start node must be triggered by this event
86
90
  return unless trigger_start_node || trigger_end_node
@@ -89,52 +93,52 @@ module Neo4j::Mapping
89
93
 
90
94
 
91
95
  def on_property_changed(node, *)
92
- trigger_rules(node) if trigger?(node)
96
+ trigger_rules(node) if trigger?(node)
93
97
  end
94
98
 
95
99
  def trigger_rules(node)
96
100
  trigger_rules_for_class(node, node[:_classname])
97
- trigger_other_rules(node)
101
+ trigger_other_rules(node)
98
102
  end
99
-
100
- def trigger_rules_for_class(node, clazz)
101
- return if @rules[clazz].nil?
103
+
104
+ def trigger_rules_for_class(node, clazz)
105
+ return if @rules[clazz].nil?
102
106
 
103
107
  agg_node = rule_for(clazz)
104
108
  @rules[clazz].each_pair do |field, rule|
105
109
  if run_rule(rule, node)
106
110
  # is this node already included ?
107
- unless connected?(field, agg_node, node)
111
+ unless connected?(field, agg_node, node)
108
112
  agg_node.outgoing(field) << node
109
113
  end
110
114
  else
111
115
  # remove old ?
112
- break_connection(field, agg_node, node)
116
+ break_connection(field, agg_node, node)
113
117
  end
114
118
  end
115
-
116
- # recursively add relationships for all the parent classes with rules that also pass for this node
117
- if clazz = eval("#{clazz}.superclass")
118
- trigger_rules_for_class(node, clazz.to_s)
119
- end
120
- end
121
-
122
- # work out if two nodes are connected by a particular relationship
123
- # uses the end_node to start with because it's more likely to have less relationships to go through
124
- # (just the number of superclasses it has really)
125
- def connected?(relationship, start_node, end_node)
126
- end_node.incoming(relationship).each do |n|
127
- return true if n == start_node
128
- end
129
- false
130
- end
131
-
132
- # sever a direct one-to-one relationship if it exists
133
- def break_connection(relationship, start_node, end_node)
134
- end_node.rels(relationship).incoming.each do |r|
135
- return r.del if r.start_node == start_node
136
- end
137
- end
119
+
120
+ # recursively add relationships for all the parent classes with rules that also pass for this node
121
+ if clazz = eval("#{clazz}.superclass")
122
+ trigger_rules_for_class(node, clazz.to_s)
123
+ end
124
+ end
125
+
126
+ # work out if two nodes are connected by a particular relationship
127
+ # uses the end_node to start with because it's more likely to have less relationships to go through
128
+ # (just the number of superclasses it has really)
129
+ def connected?(relationship, start_node, end_node)
130
+ end_node.incoming(relationship).each do |n|
131
+ return true if n == start_node
132
+ end
133
+ false
134
+ end
135
+
136
+ # sever a direct one-to-one relationship if it exists
137
+ def break_connection(relationship, start_node, end_node)
138
+ end_node.rels(relationship).incoming.each do |r|
139
+ return r.del if r.start_node == start_node
140
+ end
141
+ end
138
142
 
139
143
  def run_rule(rule, node)
140
144
  if rule.arity != 1
@@ -147,6 +151,75 @@ module Neo4j::Mapping
147
151
  end
148
152
 
149
153
 
154
+ # Allows you to group nodes by providing a rule.
155
+ #
156
+ # === Example, finding all nodes of a certain class
157
+ # Just add a rule without a code block, then all nodes of that class will be grouped under the given key (<tt>all</tt>
158
+ # for the example below).
159
+ #
160
+ # class Person
161
+ # include Neo4j::NodeMixin
162
+ # rule :all
163
+ # end
164
+ #
165
+ # Then you can get all the nodes of type Person (and siblings) by
166
+ # Person.all.each {|x| ...}
167
+ #
168
+ # === Example, finding all nodes with a given condition on a property
169
+ #
170
+ # class Person
171
+ # include Neo4j::NodeMixin
172
+ # property :age
173
+ # rule(:old) { age > 10 }
174
+ # end
175
+ #
176
+ # Now we can find all nodes with a property <tt>age</tt> above 10.
177
+ #
178
+ # === Chain Rules
179
+ #
180
+ # class NewsStory
181
+ # include Neo4j::NodeMixin
182
+ # has_n :readers
183
+ # rule(:featured) { |node| node[:featured] == true }
184
+ # rule(:young_readers) { !readers.find{|user| !user.young?}}
185
+ # end
186
+ #
187
+ # You can combine two rules. Let say you want to find all stories which are featured and has young readers:
188
+ # NewsStory.featured.young_readers.each {...}
189
+ #
190
+ # === Trigger Other Rules
191
+ # You can let one rule trigger another rule.
192
+ # Let say you have readers of some magazine and want to know if the magazine has old or young readers.
193
+ # So when a reader change from young to old you want to trigger all the magazine that he reads (a but stupid example)
194
+ #
195
+ # Example
196
+ # class Reader
197
+ # include Neo4j::NodeMixin
198
+ # property :age
199
+ # rule(:young, :trigger => :readers) { age < 15 }
200
+ # end
201
+ #
202
+ # class NewsStory
203
+ # include Neo4j::NodeMixin
204
+ # has_n :readers
205
+ # rule(:young_readers) { !readers.find{|user| !user.young?}}
206
+ # end
207
+ #
208
+ # === Performance Considerations
209
+ # If you have many rules and many updates this can be a bit slow.
210
+ # In order to speed it up somewhat you can use the raw java node object instead by providing an argument in your block.
211
+ #
212
+ # Example:
213
+ #
214
+ # class Person
215
+ # include Neo4j::NodeMixin
216
+ # property :age
217
+ # rule(:old) {|node| node[:age] > 10 }
218
+ # end
219
+ #
220
+ # === Thread Safe ?
221
+ # Not sure...
222
+ #
150
223
  module Rule
151
224
 
152
225
  # Creates an rule node attached to the Neo4j.ref_node
@@ -168,13 +241,13 @@ module Neo4j::Mapping
168
241
  # p1.young? # => true
169
242
  #
170
243
  def rule(name, props = {}, &block)
171
- singelton = class << self;
244
+ singelton = class << self;
172
245
  self;
173
246
  end
174
-
247
+
175
248
  # define class methods
176
249
  singelton.send(:define_method, name) do
177
- agg_node = Rules.rule_for(self)
250
+ agg_node = Rules.rule_for(self)
178
251
  raise "no rule node for #{name} on #{self}" if agg_node.nil?
179
252
  traversal = agg_node.outgoing(name) # TODO possible to cache this object
180
253
  Rules.fields_for(self).each do |filter_name|
@@ -188,15 +261,15 @@ module Neo4j::Mapping
188
261
  # define instance methods
189
262
  self.send(:define_method, "#{name}?") do
190
263
  instance_eval &block
191
- end
264
+ end
192
265
 
193
266
  Rules.add(self, name, props, &block)
194
267
  end
195
-
268
+
196
269
  def inherit_rules_from(clazz)
197
- Rules.inherit(clazz, self)
270
+ Rules.inherit(clazz, self)
198
271
  end
199
-
272
+
200
273
  # This is typically used for RSpecs to clean up rule nodes created by the #rule method.
201
274
  # It also remove the given class method.
202
275
  def delete_rules
@@ -209,6 +282,8 @@ module Neo4j::Mapping
209
282
  Rules.delete(self)
210
283
  end
211
284
 
285
+ # Force to trigger the rules.
286
+ # You don't normally need that since it will be done automatically.
212
287
  def trigger_rules(node)
213
288
  Rules.trigger_rules(node)
214
289
  end