neo4j 3.0.0.rc.3 → 3.0.0.rc.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,7 +3,6 @@ module Neo4j::ActiveRel
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  module ClassMethods
6
- include Enumerable
7
6
 
8
7
  # Returns the object with the specified neo4j id.
9
8
  # @param [String,Fixnum] id of node to find
@@ -13,54 +12,77 @@ module Neo4j::ActiveRel
13
12
  find_by_id(id, session)
14
13
  end
15
14
 
15
+ # Loads the relationship using its neo_id.
16
16
  def find_by_id(key, session = Neo4j::Session.current!)
17
17
  Neo4j::Relationship.load(key.to_i, session)
18
18
  end
19
19
 
20
- # TODO make this not awful
20
+ # Performs a very basic match on the relationship.
21
+ # This is not executed lazily, it will immediately return matching objects.
22
+ # To use a string, prefix the property with "r1"
23
+ # @example Match with a string
24
+ # MyRelClass.where('r1.grade > r1')
21
25
  def where(args={})
22
- @query = if self._from_class == :any
23
- Neo4j::Session.query("MATCH n1-[r1:`#{self._type}`]->(#{cypher_node_string(:inbound)}) WHERE #{where_string(args)} RETURN r1")
24
- else
25
- self._from_class.query_as(:n1).match("(#{cypher_node_string(:outbound)})-[r1:`#{self._type}`]->(#{cypher_node_string(:inbound)})").where(Hash["r1" => args])
26
- end
27
- return self
26
+ Neo4j::Session.query.match("#{cypher_string(:outbound)}-[r1:`#{self._type}`]->#{cypher_string(:inbound)}").where(where_string(args)).pluck(:r1)
28
27
  end
29
28
 
30
- def each
31
- if self._from_class == :any
32
- @query.map(&:r1)
33
- else
34
- @query.pluck(:r1)
35
- end.each {|r| yield r }
29
+ # Performs a basic match on the relationship, returning all results.
30
+ # This is not executed lazily, it will immediately return matching objects.
31
+ def all
32
+ all_query.pluck(:r1)
36
33
  end
37
34
 
38
35
  def first
39
- if self._from_class == :any
40
- @query.map(&:r1)
41
- else
42
- @query.pluck(:r1)
43
- end.first
36
+ all_query.limit(1).order("ID(r1)").pluck(:r1).first
37
+ end
38
+
39
+ def last
40
+ all_query.limit(1).order("ID(r1) DESC").pluck(:r1).first
41
+ end
42
+
43
+ private
44
+
45
+ def all_query
46
+ Neo4j::Session.query.match("#{cypher_string}-[r1:`#{self._type}`]->#{cypher_string(:inbound)}")
44
47
  end
45
48
 
46
- def cypher_node_string(dir)
49
+ def cypher_string(dir = :outbound)
47
50
  case dir
48
51
  when :outbound
49
- node_identifier, dir_class = 'n1', self._from_class
52
+ identifier = '(n1'
53
+ identifier + (_from_class == :any ? ')' : cypher_label(:outbound))
50
54
  when :inbound
51
- node_identifier, dir_class = 'n2', self._to_class
52
- end
53
- dir_class == :any ? node_identifier : "#{node_identifier}:`#{dir_class.name}`"
55
+ identifier = '(n2'
56
+ identifier + (_to_class == :any ? ')' : cypher_label(:inbound))
57
+ end
54
58
  end
55
59
 
56
- private
60
+ def cypher_label(dir = :outbound)
61
+ target_class = dir == :outbound ? as_constant(_from_class) : as_constant(_to_class)
62
+ ":`#{target_class.mapped_label_name}`)"
63
+ end
64
+
65
+ def as_constant(given_class)
66
+ case
67
+ when given_class.is_a?(String)
68
+ given_class.constantize
69
+ when given_class.is_a?(Symbol)
70
+ given_class.to_s.constantize
71
+ else
72
+ given_class
73
+ end
74
+ end
57
75
 
58
76
  def where_string(args)
59
- args.map do |k, v|
60
- v.is_a?(Integer) ? "r1.#{k} = #{v}" : "r1.#{k} = '#{v}'"
61
- end.join(', ')
77
+ if args.is_a?(Hash)
78
+ args.map do |k, v|
79
+ v.is_a?(Integer) ? "r1.#{k} = #{v}" : "r1.#{k} = '#{v}'"
80
+ end.join(', ')
81
+ else
82
+ args
83
+ end
62
84
  end
63
85
 
64
86
  end
65
87
  end
66
- end
88
+ end
@@ -2,14 +2,10 @@ module Neo4j::ActiveRel
2
2
  # A container for ActiveRel's :inbound and :outbound methods. It provides lazy loading of nodes.
3
3
  class RelatedNode
4
4
 
5
- class InvalidParameterError < StandardError
6
- def message
7
- 'RelatedNode must be initialized with either a node ID or node'
8
- end
9
- end
5
+ class InvalidParameterError < StandardError; end
10
6
 
11
7
  def initialize(node = nil)
12
- @node = valid_node_param?(node) ? node : (raise InvalidParameterError.new(self))
8
+ @node = valid_node_param?(node) ? node : (raise InvalidParameterError, 'RelatedNode must be initialized with either a node ID or node' )
13
9
  end
14
10
 
15
11
  def == (obj)
@@ -0,0 +1,185 @@
1
+ module Neo4j
2
+ class Migration
3
+ class AddIdProperty < Neo4j::Migration
4
+ attr_reader :models_filename
5
+
6
+ def initialize
7
+ @models_filename = File.join(Rails.root.join('db', 'neo4j-migrate'), 'add_id_property.yml')
8
+ end
9
+
10
+ def migrate
11
+ models = ActiveSupport::HashWithIndifferentAccess.new(YAML.load_file(models_filename))[:models]
12
+ puts "This task will add an ID Property every node in the given file."
13
+ puts "It may take a significant amount of time, please be patient."
14
+ models.each do |model|
15
+ puts
16
+ puts
17
+ puts "Adding IDs to #{model}"
18
+ add_ids_to model.constantize
19
+ end
20
+ end
21
+
22
+ def setup
23
+ FileUtils.mkdir_p("db/neo4j-migrate")
24
+ unless File.file?(models_filename)
25
+ File.open(models_filename, 'w') do |file|
26
+ file.write("# Provide models to which IDs should be added.\n# It will only modify nodes that do not have IDs. There is no danger of overwriting data.\n# models: [Student,Lesson,Teacher,Exam]\nmodels: []")
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def add_ids_to(model)
34
+ require 'benchmark'
35
+
36
+ max_per_batch = (ENV['MAX_PER_BATCH'] || default_max_per_batch).to_i
37
+
38
+ label = model.mapped_label_name
39
+ property = model.primary_key
40
+ nodes_left = 1
41
+ last_time_taken = nil
42
+
43
+ until nodes_left == 0
44
+ nodes_left = Neo4j::Session.query.match(n: label).where("NOT has(n.#{property})").return("COUNT(n) AS ids").first.ids
45
+
46
+ time_per_node = last_time_taken / max_per_batch if last_time_taken
47
+ print "Running first batch...\r"
48
+ if time_per_node
49
+ eta_seconds = (nodes_left * time_per_node).round
50
+ print "#{nodes_left} nodes left. Last batch: #{(time_per_node * 1000.0).round(1)}ms / node (ETA: #{eta_seconds / 60} minutes)\r"
51
+ end
52
+
53
+ return if nodes_left == 0
54
+ to_set = [nodes_left, max_per_batch].min
55
+
56
+ new_ids = to_set.times.map { new_id_for(model) }
57
+ begin
58
+ last_time_taken = id_batch_set(label, property, new_ids, to_set)
59
+ rescue Neo4j::Server::CypherResponse::ResponseError, Faraday::TimeoutError
60
+ new_max_per_batch = (max_per_batch * 0.8).round
61
+ puts "Error querying #{max_per_batch} nodes. Trying #{new_max_per_batch}"
62
+ max_per_batch = new_max_per_batch
63
+ end
64
+ end
65
+ end
66
+
67
+ def id_batch_set(label, property, new_ids, to_set)
68
+ Benchmark.realtime do
69
+ Neo4j::Transaction.run do
70
+ Neo4j::Session.query("MATCH (n:`#{label}`) WHERE NOT has(n.#{property})
71
+ with COLLECT(n) as nodes, #{new_ids} as ids
72
+ FOREACH(i in range(0,#{to_set - 1})|
73
+ FOREACH(node in [nodes[i]]|
74
+ SET node.#{property} = ids[i]))
75
+ RETURN distinct(true)
76
+ LIMIT #{to_set}")
77
+ end
78
+ end
79
+ end
80
+
81
+ def default_max_per_batch
82
+ 900
83
+ end
84
+
85
+ def new_id_for(model)
86
+ if model.id_property_info[:type][:auto]
87
+ SecureRandom::uuid
88
+ else
89
+ model.new.send(model.id_property_info[:type][:on])
90
+ end
91
+ end
92
+ end
93
+
94
+ class AddClassnames < Neo4j::Migration
95
+ attr_reader :classnames_filename, :classnames_filepath
96
+
97
+ def initialize
98
+ @classnames_filename = 'add_classnames.yml'
99
+ @classnames_filepath = File.join(Rails.root.join('db', 'neo4j-migrate'), classnames_filename)
100
+ end
101
+
102
+ def migrate
103
+ puts "Adding classnames. This make take some time."
104
+ execute(true)
105
+ end
106
+
107
+ def test
108
+ puts "TESTING! No queries will be executed."
109
+ execute(false)
110
+ end
111
+
112
+ def setup
113
+ puts "Creating file #{classnames_filepath}. Please use this as the migration guide."
114
+ FileUtils.mkdir_p("db/neo4j-migrate")
115
+ unless File.file?(@classnames_filepath)
116
+ source = File.join(File.dirname(__FILE__), "..", "..", "config", "neo4j", classnames_filename)
117
+ FileUtils.copy_file(source, classnames_filepath)
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def execute(migrate = false)
124
+ file_init
125
+ map = []
126
+ map.push :nodes if @model_map[:nodes]
127
+ map.push :relationships if @model_map[:relationships]
128
+ map.each do |type|
129
+ @model_map[type].each do |action, labels|
130
+ do_classnames(action, labels, type, migrate)
131
+ end
132
+ end
133
+ end
134
+
135
+ def do_classnames(action, labels, type, migrate = false)
136
+ method = type == :nodes ? :node_cypher : :rel_cypher
137
+ labels.each do |label|
138
+ puts cypher = self.send(method, label, action)
139
+ execute_cypher(cypher) if migrate
140
+ end
141
+ end
142
+
143
+ def file_init
144
+ @model_map = ActiveSupport::HashWithIndifferentAccess.new(YAML.load_file(classnames_filepath))
145
+ end
146
+
147
+ def node_cypher(label, action)
148
+ where, phrase_start = action_variables(action, 'n')
149
+ puts "#{phrase_start} _classname '#{label}' on nodes with matching label:"
150
+ "MATCH (n:`#{label}`) #{where} SET n._classname = '#{label}' RETURN COUNT(n) as modified"
151
+ end
152
+
153
+ def rel_cypher(hash, action)
154
+ label = hash[0]
155
+ value = hash[1]
156
+ from = value[:from]
157
+ raise "All relationships require a 'type'" unless value[:type]
158
+
159
+ from_cypher = from ? "(from:`#{from}`)" : "(from)"
160
+ to = value[:to]
161
+ to_cypher = to ? "(to:`#{to}`)" : "(to)"
162
+ type = "[r:`#{value[:type]}`]"
163
+ where, phrase_start = action_variables(action, 'r')
164
+ puts "#{phrase_start} _classname '#{label}' where type is '#{value[:type]}' using cypher:"
165
+ "MATCH #{from_cypher}-#{type}->#{to_cypher} #{where} SET r._classname = '#{label}' return COUNT(r) as modified"
166
+ end
167
+
168
+ def execute_cypher(query_string)
169
+ puts "Modified #{Neo4j::Session.query(query_string).first.modified} records"
170
+ puts ""
171
+ end
172
+
173
+ def action_variables(action, identifier)
174
+ case action
175
+ when 'overwrite'
176
+ ['', 'Overwriting']
177
+ when 'add'
178
+ ["WHERE NOT HAS(#{identifier}._classname)", 'Adding']
179
+ else
180
+ raise "Invalid action #{action} specified"
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -10,10 +10,11 @@ module Neo4j
10
10
  def self.create_from(source, page, per_page)
11
11
  #partial = source.drop((page-1) * per_page).first(per_page)
12
12
  partial = source.skip(page-1).limit(per_page)
13
- Paginated.new(partial, source.instance_variable_get(:@model).count, page)
13
+ Paginated.new(partial, source.count, page)
14
14
  end
15
15
 
16
16
  delegate :each, :to => :items
17
+ delegate :pluck, :to => :items
17
18
  delegate :size, :[], :to => :items
18
19
  end
19
20
  end
data/lib/neo4j/railtie.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  require 'active_support/notifications'
2
+ require 'rails/railtie'
2
3
 
3
4
  module Neo4j
4
- class Railtie < ::Rails::Railtie
5
+ class Railtie < ::Rails::Railtie
5
6
  config.neo4j = ActiveSupport::OrderedOptions.new
6
7
 
7
8
  # Add ActiveModel translations to the I18n load_path
@@ -9,6 +10,11 @@ module Neo4j
9
10
  config.i18n.load_path += Dir[File.join(File.dirname(__FILE__), '..', '..', '..', 'config', 'locales', '*.{rb,yml}')]
10
11
  end
11
12
 
13
+ rake_tasks do
14
+ load 'neo4j/tasks/neo4j_server.rake'
15
+ load 'neo4j/tasks/migration.rake'
16
+ end
17
+
12
18
  class << self
13
19
  def java_platform?
14
20
  RUBY_PLATFORM =~ /java/
@@ -26,7 +32,7 @@ module Neo4j
26
32
  end
27
33
 
28
34
  if cfg.sessions.empty?
29
- cfg.sessions << {type: cfg.session_type, path: cfg.session_path, options: cfg.session_options}
35
+ cfg.sessions << { type: cfg.session_type, path: cfg.session_path, options: cfg.session_options }
30
36
  end
31
37
  end
32
38
 
@@ -45,18 +51,17 @@ module Neo4j
45
51
  raise "Tried to start embedded Neo4j db without using JRuby (got #{RUBY_PLATFORM}), please run `rvm jruby`"
46
52
  end
47
53
 
48
- if (session_opts.key? :name)
49
- session = Neo4j::Session.open_named(session_opts[:type], session_opts[:name], session_opts[:default], session_opts[:path])
50
- else
51
- session = Neo4j::Session.open(session_opts[:type], session_opts[:path], session_opts[:options])
52
- end
54
+ session = if session_opts.key?(:name)
55
+ Neo4j::Session.open_named(session_opts[:type], session_opts[:name], session_opts[:default], session_opts[:path])
56
+ else
57
+ Neo4j::Session.open(session_opts[:type], session_opts[:path], session_opts[:options])
58
+ end
53
59
 
54
60
  start_embedded_session(session) if session_opts[:type] == :embedded_db
55
61
  end
56
62
 
57
63
  end
58
64
 
59
-
60
65
  # Starting Neo after :load_config_initializers allows apps to
61
66
  # register migrations in config/initializers
62
67
  initializer "neo4j.start", :after => :load_config_initializers do |app|
@@ -1,45 +1,9 @@
1
1
  module Neo4j::Shared
2
2
  module Persistence
3
3
 
4
- class RecordInvalidError < RuntimeError
5
- attr_reader :record
6
-
7
- def initialize(record)
8
- @record = record
9
- super(@record.errors.full_messages.join(", "))
10
- end
11
- end
12
-
13
4
  extend ActiveSupport::Concern
14
5
  include Neo4j::TypeConverters
15
6
 
16
- # Saves the model.
17
- #
18
- # If the model is new a record gets created in the database, otherwise the existing record gets updated.
19
- # If perform_validation is true validations run.
20
- # If any of them fail the action is cancelled and save returns false. If the flag is false validations are bypassed altogether. See ActiveRecord::Validations for more information.
21
- # There’s a series of callbacks associated with save. If any of the before_* callbacks return false the action is cancelled and save returns false.
22
- def save(*)
23
- update_magic_properties
24
- create_or_update
25
- end
26
-
27
- # Persist the object to the database. Validations and Callbacks are included
28
- # by default but validation can be disabled by passing :validate => false
29
- # to #save! Creates a new transaction.
30
- #
31
- # @raise a RecordInvalidError if there is a problem during save.
32
- # @param (see Neo4j::Rails::Validations#save)
33
- # @return nil
34
- # @see #save
35
- # @see Neo4j::Rails::Validations Neo4j::Rails::Validations - for the :validate parameter
36
- # @see Neo4j::Rails::Callbacks Neo4j::Rails::Callbacks - for callbacks
37
- def save!(*args)
38
- unless save(*args)
39
- raise RecordInvalidError.new(self)
40
- end
41
- end
42
-
43
7
  def update_model
44
8
  if changed_attributes && !changed_attributes.empty?
45
9
  changed_props = attributes.select{|k,v| changed_attributes.include?(k)}
@@ -49,7 +13,6 @@ module Neo4j::Shared
49
13
  end
50
14
  end
51
15
 
52
-
53
16
  # Convenience method to set attribute and #save at the same time
54
17
  # @param [Symbol, String] attribute of the attribute to update
55
18
  # @param [Object] value to set
@@ -66,13 +29,6 @@ module Neo4j::Shared
66
29
  self.save!
67
30
  end
68
31
 
69
- # Convenience method to set multiple attributes and #save at the same time
70
- # @param [Hash] attributes of names and values of attributes to set
71
- def update_attributes(attributes)
72
- assign_attributes(attributes)
73
- self.save
74
- end
75
-
76
32
  def create_or_update
77
33
  # since the same model can be created or updated twice from a relationship we have to have this guard
78
34
  @_create_or_updating = true
@@ -145,6 +101,7 @@ module Neo4j::Shared
145
101
 
146
102
  def reload
147
103
  return self if new_record?
104
+ clear_association_cache
148
105
  changed_attributes && changed_attributes.clear
149
106
  unless reload_from_database
150
107
  @_deleted = true
@@ -164,30 +121,34 @@ module Neo4j::Shared
164
121
  # Updates this resource with all the attributes from the passed-in Hash and requests that the record be saved.
165
122
  # If saving fails because the resource is invalid then false will be returned.
166
123
  def update(attributes)
167
- self.attributes = attributes
124
+ self.attributes = process_attributes(attributes)
168
125
  save
169
126
  end
170
127
  alias_method :update_attributes, :update
171
128
 
172
129
  # Same as {#update_attributes}, but raises an exception if saving fails.
173
130
  def update!(attributes)
174
- self.attributes = attributes
131
+ self.attributes = process_attributes(attributes)
175
132
  save!
176
133
  end
177
134
  alias_method :update_attributes!, :update!
178
135
 
179
136
  def cache_key
180
137
  if self.new_record?
181
- "#{self.class.model_name.cache_key}/new"
138
+ "#{model_cache_key}/new"
182
139
  elsif self.respond_to?(:updated_at) && !self.updated_at.blank?
183
- "#{self.class.model_name.cache_key}/#{neo_id}-#{self.updated_at.utc.to_s(:number)}"
140
+ "#{model_cache_key}/#{neo_id}-#{self.updated_at.utc.to_s(:number)}"
184
141
  else
185
- "#{self.class.model_name.cache_key}/#{neo_id}"
142
+ "#{model_cache_key}/#{neo_id}"
186
143
  end
187
144
  end
188
145
 
189
146
  private
190
147
 
148
+ def model_cache_key
149
+ self.class.model_name.cache_key
150
+ end
151
+
191
152
  def create_magic_properties
192
153
  end
193
154
 
@@ -199,16 +160,9 @@ module Neo4j::Shared
199
160
  props[:_classname] = self.class.name if self.class.cached_class?
200
161
  end
201
162
 
202
- # def assign_attributes(attributes)
203
- # attributes.each do |attribute, value|
204
- # send("#{attribute}=", value)
205
- # end
206
- # end
207
-
208
163
  def set_timestamps
209
164
  self.created_at = DateTime.now if respond_to?(:created_at=)
210
165
  self.updated_at = self.created_at if respond_to?(:updated_at=)
211
166
  end
212
-
213
167
  end
214
168
  end