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

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.
@@ -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