neo4j 4.1.5 → 5.0.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +584 -0
  3. data/CONTRIBUTORS +7 -28
  4. data/Gemfile +6 -1
  5. data/README.md +54 -8
  6. data/lib/neo4j.rb +5 -0
  7. data/lib/neo4j/active_node.rb +1 -0
  8. data/lib/neo4j/active_node/dependent/association_methods.rb +35 -17
  9. data/lib/neo4j/active_node/dependent/query_proxy_methods.rb +21 -19
  10. data/lib/neo4j/active_node/has_n.rb +377 -132
  11. data/lib/neo4j/active_node/has_n/association.rb +77 -38
  12. data/lib/neo4j/active_node/id_property.rb +46 -28
  13. data/lib/neo4j/active_node/initialize.rb +18 -6
  14. data/lib/neo4j/active_node/labels.rb +69 -35
  15. data/lib/neo4j/active_node/node_wrapper.rb +37 -30
  16. data/lib/neo4j/active_node/orm_adapter.rb +5 -4
  17. data/lib/neo4j/active_node/persistence.rb +53 -10
  18. data/lib/neo4j/active_node/property.rb +13 -5
  19. data/lib/neo4j/active_node/query.rb +11 -10
  20. data/lib/neo4j/active_node/query/query_proxy.rb +126 -153
  21. data/lib/neo4j/active_node/query/query_proxy_enumerable.rb +15 -25
  22. data/lib/neo4j/active_node/query/query_proxy_link.rb +89 -0
  23. data/lib/neo4j/active_node/query/query_proxy_methods.rb +72 -19
  24. data/lib/neo4j/active_node/query_methods.rb +3 -1
  25. data/lib/neo4j/active_node/scope.rb +17 -21
  26. data/lib/neo4j/active_node/validations.rb +8 -2
  27. data/lib/neo4j/active_rel/initialize.rb +1 -2
  28. data/lib/neo4j/active_rel/persistence.rb +21 -33
  29. data/lib/neo4j/active_rel/property.rb +4 -2
  30. data/lib/neo4j/active_rel/types.rb +20 -8
  31. data/lib/neo4j/config.rb +16 -6
  32. data/lib/neo4j/core/query.rb +2 -2
  33. data/lib/neo4j/errors.rb +10 -0
  34. data/lib/neo4j/migration.rb +57 -46
  35. data/lib/neo4j/paginated.rb +3 -1
  36. data/lib/neo4j/railtie.rb +26 -14
  37. data/lib/neo4j/shared.rb +7 -1
  38. data/lib/neo4j/shared/declared_property.rb +62 -0
  39. data/lib/neo4j/shared/declared_property_manager.rb +150 -0
  40. data/lib/neo4j/shared/persistence.rb +15 -8
  41. data/lib/neo4j/shared/property.rb +64 -49
  42. data/lib/neo4j/shared/rel_type_converters.rb +13 -12
  43. data/lib/neo4j/shared/serialized_properties.rb +0 -15
  44. data/lib/neo4j/shared/type_converters.rb +53 -47
  45. data/lib/neo4j/shared/typecaster.rb +49 -0
  46. data/lib/neo4j/version.rb +1 -1
  47. data/lib/rails/generators/neo4j/model/model_generator.rb +3 -3
  48. data/lib/rails/generators/neo4j_generator.rb +5 -12
  49. data/neo4j.gemspec +4 -3
  50. metadata +30 -11
  51. data/CHANGELOG +0 -545
@@ -29,8 +29,10 @@ module Neo4j::ActiveRel
29
29
  # Extracts keys from attributes hash which are relationships of the model
30
30
  # TODO: Validate separately that relationships are getting the right values? Perhaps also store the values and persist relationships on save?
31
31
  def extract_association_attributes!(attributes)
32
- attributes.keys.each_with_object({}) do |key, relationship_props|
33
- relationship_props[key] = attributes.delete(key) if [:from_node, :to_node].include?(key)
32
+ {}.tap do |relationship_props|
33
+ attributes.each_key do |key|
34
+ relationship_props[key] = attributes.delete(key) if [:from_node, :to_node].include?(key)
35
+ end
34
36
  end
35
37
  end
36
38
 
@@ -21,25 +21,37 @@ module Neo4j
21
21
  WRAPPED_CLASSES = {}
22
22
 
23
23
  included do
24
- type self.name, true
24
+ type self.namespaced_model_name, true
25
25
  end
26
26
 
27
27
  module ClassMethods
28
28
  include Neo4j::Shared::RelTypeConverters
29
29
 
30
- def inherited(other)
31
- other.type other.name, true
30
+ def inherited(subclass)
31
+ subclass.type subclass.namespaced_model_name, true
32
32
  end
33
33
 
34
34
  # @param type [String] sets the relationship type when creating relationships via this class
35
- def type(given_type = self.name, auto = false)
36
- use_type = auto ? decorated_rel_type(given_type) : given_type
37
- add_wrapped_class use_type
38
- @rel_type = use_type
35
+ def type(given_type = namespaced_model_name, auto = false)
36
+ @rel_type = (auto ? decorated_rel_type(given_type) : given_type).tap do |type|
37
+ add_wrapped_class type unless uses_classname?
38
+ end
39
+ end
40
+
41
+ def namespaced_model_name
42
+ case Neo4j::Config[:module_handling]
43
+ when :demodulize
44
+ self.name.demodulize
45
+ when Proc
46
+ Neo4j::Config[:module_handling].call(self.name)
47
+ else
48
+ self.name
49
+ end
39
50
  end
40
51
 
41
- # @return [String] a string representing the relationship type that will be created
42
52
  attr_reader :rel_type
53
+ # @return [String] a string representing the relationship type that will be created
54
+ # attr_reader :rel_type
43
55
  alias_method :_type, :rel_type # Should be deprecated
44
56
 
45
57
  def add_wrapped_class(type)
@@ -5,6 +5,7 @@ module Neo4j
5
5
  #
6
6
  class Config
7
7
  DEFAULT_FILE = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'config', 'neo4j', 'config.yml'))
8
+ CLASS_NAME_PROPERTY_KEY = 'class_name_property'
8
9
 
9
10
  class << self
10
11
  # @return [Fixnum] The location of the default configuration file.
@@ -53,7 +54,6 @@ module Neo4j
53
54
  nil
54
55
  end
55
56
 
56
-
57
57
  # Sets the value of a config entry.
58
58
  #
59
59
  # @param [Symbol] key the key to set the parameter for
@@ -62,14 +62,12 @@ module Neo4j
62
62
  configuration[key.to_s] = val
63
63
  end
64
64
 
65
-
66
65
  # @param [Symbol] key The key of the config entry value we want
67
66
  # @return the the value of a config entry
68
67
  def [](key)
69
68
  configuration[key.to_s]
70
69
  end
71
70
 
72
-
73
71
  # Remove the value of a config entry.
74
72
  #
75
73
  # @param [Symbol] key the key of the configuration entry to delete
@@ -78,7 +76,6 @@ module Neo4j
78
76
  configuration.delete(key)
79
77
  end
80
78
 
81
-
82
79
  # Remove all configuration. This can be useful for testing purpose.
83
80
  #
84
81
  # @return nil
@@ -86,7 +83,6 @@ module Neo4j
86
83
  @configuration = nil
87
84
  end
88
85
 
89
-
90
86
  # @return [Hash] The config as a hash.
91
87
  def to_hash
92
88
  configuration.to_hash
@@ -98,13 +94,27 @@ module Neo4j
98
94
  end
99
95
 
100
96
  def class_name_property
101
- Neo4j::Config[:class_name_property] || :_classname
97
+ @_class_name_property = Neo4j::Config[CLASS_NAME_PROPERTY_KEY] || :_classname
102
98
  end
103
99
 
104
100
  def include_root_in_json
105
101
  # we use ternary because a simple || will always evaluate true
106
102
  Neo4j::Config[:include_root_in_json].nil? ? true : Neo4j::Config[:include_root_in_json]
107
103
  end
104
+
105
+ def module_handling
106
+ Neo4j::Config[:module_handling] || :none
107
+ end
108
+
109
+ def association_model_namespace
110
+ Neo4j::Config[:association_model_namespace] || nil
111
+ end
112
+
113
+ def association_model_namespace_string
114
+ namespace = Neo4j::Config[:association_model_namespace]
115
+ return nil if namespace.nil?
116
+ "::#{namespace}"
117
+ end
108
118
  end
109
119
  end
110
120
  end
@@ -8,14 +8,14 @@ module Neo4j::Core
8
8
  # @return [Neo4j::ActiveNode::Query::QueryProxy] A QueryProxy object.
9
9
  def proxy_as(model, var, optional = false)
10
10
  # TODO: Discuss whether it's necessary to call `break` on the query or if this should be left to the user.
11
- Neo4j::ActiveNode::Query::QueryProxy.new(model, nil, starting_query: self, node: var, optional: optional, chain_level: @proxy_chain_level)
11
+ Neo4j::ActiveNode::Query::QueryProxy.new(model, nil, node: var, optional: optional, starting_query: self, chain_level: @proxy_chain_level)
12
12
  end
13
13
 
14
14
  # Calls proxy_as with `optional` set true. This doesn't offer anything different from calling `proxy_as` directly but it may be more readable.
15
15
  def proxy_as_optional(model, var)
16
16
  proxy_as(model, var, true)
17
17
  end
18
- #
18
+
19
19
  # For instances where you turn a QueryProxy into a Query and then back to a QueryProxy with `#proxy_as`
20
20
  attr_accessor :proxy_chain_level
21
21
  end
@@ -0,0 +1,10 @@
1
+ module Neo4j
2
+ # Neo4j.rb Errors
3
+ # Generic Neo4j.rb exception class.
4
+ class Neo4jrbError < StandardError
5
+ end
6
+
7
+ # Raised when Neo4j.rb cannot find record by given id.
8
+ class RecordNotFound < Neo4jrbError
9
+ end
10
+ end
@@ -1,3 +1,5 @@
1
+ require 'benchmark'
2
+
1
3
  module Neo4j
2
4
  class Migration
3
5
  def migrate
@@ -17,7 +19,7 @@ module Neo4j
17
19
  end
18
20
 
19
21
  def joined_path(path)
20
- File.join(path, 'db', 'neo4j-migrate')
22
+ File.join(path.to_s, 'db', 'neo4j-migrate')
21
23
  end
22
24
 
23
25
  class AddIdProperty < Neo4j::Migration
@@ -41,64 +43,72 @@ module Neo4j
41
43
 
42
44
  def setup
43
45
  FileUtils.mkdir_p('db/neo4j-migrate')
44
- unless File.file?(models_filename)
45
- File.open(models_filename, 'w') do |file|
46
- 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: []")
47
- end
46
+
47
+ return if File.file?(models_filename)
48
+
49
+ File.open(models_filename, 'w') do |file|
50
+ message = <<MESSAGE
51
+ # Provide models to which IDs should be added.
52
+ # # It will only modify nodes that do not have IDs. There is no danger of overwriting data.
53
+ # # models: [Student,Lesson,Teacher,Exam]\nmodels: []
54
+ MESSAGE
55
+ file.write(message)
48
56
  end
49
57
  end
50
58
 
51
59
  private
52
60
 
53
61
  def add_ids_to(model)
54
- require 'benchmark'
55
-
56
62
  max_per_batch = (ENV['MAX_PER_BATCH'] || default_max_per_batch).to_i
57
63
 
58
64
  label = model.mapped_label_name
59
- property = model.primary_key
60
- nodes_left = 1
61
65
  last_time_taken = nil
62
66
 
63
- until nodes_left == 0
64
- nodes_left = Neo4j::Session.query.match(n: label).where("NOT has(n.#{property})").return('COUNT(n) AS ids').first.ids
67
+ until (nodes_left = idless_count(label, model.primary_key)) == 0
68
+ print_status(last_time_taken, max_per_batch, nodes_left)
65
69
 
66
- time_per_node = last_time_taken / max_per_batch if last_time_taken
67
- print_output "Running first batch...\r"
68
- if time_per_node
69
- eta_seconds = (nodes_left * time_per_node).round
70
- print_output "#{nodes_left} nodes left. Last batch: #{(time_per_node * 1000.0).round(1)}ms / node (ETA: #{eta_seconds / 60} minutes)\r"
70
+ count = [nodes_left, max_per_batch].min
71
+ last_time_taken = Benchmark.realtime do
72
+ max_per_batch = id_batch_set(label, model.primary_key, count.times.map { new_id_for(model) }, count)
71
73
  end
74
+ end
75
+ end
72
76
 
73
- return if nodes_left == 0
74
- to_set = [nodes_left, max_per_batch].min
77
+ def idless_count(label, id_property)
78
+ Neo4j::Session.query.match(n: label).where("NOT has(n.#{id_property})").pluck('COUNT(n) AS ids').first
79
+ end
75
80
 
76
- new_ids = to_set.times.map { new_id_for(model) }
77
- begin
78
- last_time_taken = id_batch_set(label, property, new_ids, to_set)
79
- rescue Neo4j::Server::CypherResponse::ResponseError, Faraday::TimeoutError
80
- new_max_per_batch = (max_per_batch * 0.8).round
81
- output "Error querying #{max_per_batch} nodes. Trying #{new_max_per_batch}"
82
- max_per_batch = new_max_per_batch
83
- end
84
- end
81
+ def print_status(last_time_taken, max_per_batch, nodes_left)
82
+ time_per_node = last_time_taken / max_per_batch if last_time_taken
83
+ message = if time_per_node
84
+ eta_seconds = (nodes_left * time_per_node).round
85
+ "#{nodes_left} nodes left. Last batch: #{(time_per_node * 1000.0).round(1)}ms / node (ETA: #{eta_seconds / 60} minutes)\r"
86
+ else
87
+ "Running first batch...\r"
88
+ end
89
+
90
+ print_output message
85
91
  end
86
92
 
87
- def id_batch_set(label, property, new_ids, to_set)
88
- Benchmark.realtime do
89
- begin
90
- tx = Neo4j::Transaction.new
91
- Neo4j::Session.query("MATCH (n:`#{label}`) WHERE NOT has(n.#{property})
92
- with COLLECT(n) as nodes, #{new_ids} as ids
93
- FOREACH(i in range(0,#{to_set - 1})|
94
- FOREACH(node in [nodes[i]]|
95
- SET node.#{property} = ids[i]))
96
- RETURN distinct(true)
97
- LIMIT #{to_set}")
98
- ensure
99
- tx.close
100
- end
101
- end
93
+
94
+ def id_batch_set(label, id_property, new_ids, count)
95
+ tx = Neo4j::Transaction.new
96
+
97
+ Neo4j::Session.query("MATCH (n:`#{label}`) WHERE NOT has(n.#{id_property})
98
+ with COLLECT(n) as nodes, #{new_ids} as ids
99
+ FOREACH(i in range(0,#{count - 1})|
100
+ FOREACH(node in [nodes[i]]|
101
+ SET node.#{id_property} = ids[i]))
102
+ RETURN distinct(true)
103
+ LIMIT #{count}")
104
+
105
+ count
106
+ rescue Neo4j::Server::CypherResponse::ResponseError, Faraday::TimeoutError
107
+ new_max_per_batch = (max_per_batch * 0.8).round
108
+ output "Error querying #{max_per_batch} nodes. Trying #{new_max_per_batch}"
109
+ new_max_per_batch
110
+ ensure
111
+ tx.close
102
112
  end
103
113
 
104
114
  def default_max_per_batch
@@ -133,10 +143,11 @@ module Neo4j
133
143
  def setup
134
144
  output "Creating file #{classnames_filepath}. Please use this as the migration guide."
135
145
  FileUtils.mkdir_p('db/neo4j-migrate')
136
- unless File.file?(classnames_filepath)
137
- source = File.join(File.dirname(__FILE__), '..', '..', 'config', 'neo4j', classnames_filename)
138
- FileUtils.copy_file(source, classnames_filepath)
139
- end
146
+
147
+ return if File.file?(classnames_filepath)
148
+
149
+ source = File.join(File.dirname(__FILE__), '..', '..', 'config', 'neo4j', classnames_filename)
150
+ FileUtils.copy_file(source, classnames_filepath)
140
151
  end
141
152
 
142
153
  private
@@ -4,7 +4,9 @@ module Neo4j
4
4
  attr_reader :items, :total, :current_page
5
5
 
6
6
  def initialize(items, total, current_page)
7
- @items, @total, @current_page = items, total, current_page
7
+ @items = items
8
+ @total = total
9
+ @current_page = current_page
8
10
  end
9
11
 
10
12
  def self.create_from(source, page, per_page, order = nil)
@@ -21,19 +21,24 @@ module Neo4j
21
21
  end
22
22
 
23
23
  def setup_default_session(cfg)
24
+ setup_config_defaults!(cfg)
25
+
26
+ return if !cfg.sessions.empty?
27
+
28
+ cfg.sessions << {type: cfg.session_type, path: cfg.session_path, options: cfg.session_options}
29
+ end
30
+
31
+ def setup_config_defaults!(cfg)
24
32
  cfg.session_type ||= :server_db
25
33
  cfg.session_path ||= 'http://localhost:7474'
26
34
  cfg.session_options ||= {}
27
35
  cfg.sessions ||= []
28
36
 
29
- unless (uri = URI(cfg.session_path)).user.blank?
30
- cfg.session_options.reverse_merge!(basic_auth: {username: uri.user, password: uri.password})
31
- cfg.session_path = cfg.session_path.gsub("#{uri.user}:#{uri.password}@", '')
32
- end
37
+ uri = URI(cfg.session_path)
38
+ return if uri.user.blank?
33
39
 
34
- if cfg.sessions.empty?
35
- cfg.sessions << {type: cfg.session_type, path: cfg.session_path, options: cfg.session_options}
36
- end
40
+ cfg.session_options.reverse_merge!(basic_auth: {username: uri.user, password: uri.password})
41
+ cfg.session_path = cfg.session_path.gsub("#{uri.user}:#{uri.password}@", '')
37
42
  end
38
43
 
39
44
 
@@ -63,6 +68,16 @@ module Neo4j
63
68
  end
64
69
  end
65
70
 
71
+ def register_neo4j_cypher_logging
72
+ return if @neo4j_cypher_logging_registered
73
+
74
+ Neo4j::Server::CypherSession.log_with do |message|
75
+ Rails.logger.info message
76
+ end
77
+
78
+ @neo4j_cypher_logging_registered = true
79
+ end
80
+
66
81
  # Starting Neo after :load_config_initializers allows apps to
67
82
  # register migrations in config/initializers
68
83
  initializer 'neo4j.start', after: :load_config_initializers do |app|
@@ -75,14 +90,11 @@ module Neo4j
75
90
  end
76
91
  Neo4j::Config.configuration.merge!(cfg.to_hash)
77
92
 
78
- clear = "\e[0m"
79
- yellow = "\e[33m"
80
- cyan = "\e[36m"
93
+ register_neo4j_cypher_logging
94
+ end
81
95
 
82
- ActiveSupport::Notifications.subscribe('neo4j.cypher_query') do |_, start, finish, _id, payload|
83
- ms = (finish - start) * 1000
84
- Rails.logger.info " #{cyan}#{payload[:context]}#{clear} #{yellow}#{ms.round}ms#{clear} #{payload[:cypher]}" + (payload[:params].size > 0 ? ' | ' + payload[:params].inspect : '')
85
- end
96
+ console do
97
+ register_neo4j_cypher_logging
86
98
  end
87
99
  end
88
100
  end
@@ -18,7 +18,8 @@ module Neo4j
18
18
 
19
19
  def neo4j_session
20
20
  if @neo4j_session_name
21
- Neo4j::Session.named(@neo4j_session_name) || fail("#{self.name} is configured to use a neo4j session named #{@neo4j_session_name}, but no such session is registered with Neo4j::Session")
21
+ Neo4j::Session.named(@neo4j_session_name) ||
22
+ fail("#{self.name} is configured to use a neo4j session named #{@neo4j_session_name}, but no such session is registered with Neo4j::Session")
22
23
  else
23
24
  Neo4j::Session.current!
24
25
  end
@@ -27,10 +28,15 @@ module Neo4j
27
28
 
28
29
  included do
29
30
  self.include_root_in_json = Neo4j::Config.include_root_in_json
31
+ @_declared_property_manager ||= Neo4j::Shared::DeclaredPropertyManager.new(self)
30
32
 
31
33
  def self.i18n_scope
32
34
  :neo4j
33
35
  end
34
36
  end
37
+
38
+ def declared_property_manager
39
+ self.class.declared_property_manager
40
+ end
35
41
  end
36
42
  end
@@ -0,0 +1,62 @@
1
+ module Neo4j::Shared
2
+ # Contains methods related to the management
3
+ class DeclaredProperty
4
+ class IllegalPropertyError < StandardError; end
5
+
6
+ ILLEGAL_PROPS = %w(from_node to_node start_node end_node)
7
+ attr_reader :name, :name_string, :name_sym, :options, :magic_typecaster
8
+
9
+ def initialize(name, options = {})
10
+ fail IllegalPropertyError, "#{name} is an illegal property" if ILLEGAL_PROPS.include?(name.to_s)
11
+ @name = @name_sym = name
12
+ @name_string = name.to_s
13
+ @options = options
14
+ end
15
+
16
+ def register
17
+ register_magic_properties
18
+ end
19
+
20
+ def type
21
+ options[:type]
22
+ end
23
+
24
+ def typecaster
25
+ options[:typecaster]
26
+ end
27
+
28
+ def default_value
29
+ options[:default]
30
+ end
31
+
32
+ private
33
+
34
+ # Tweaks properties
35
+ def register_magic_properties
36
+ options[:type] ||= DateTime if name.to_sym == :created_at || name.to_sym == :updated_at
37
+ # TODO: Custom typecaster to fix the stuff below
38
+ # ActiveAttr does not handle "Time", Rails and Neo4j.rb 2.3 did
39
+ # Convert it to DateTime in the interest of consistency
40
+ options[:type] = DateTime if options[:type] == Time
41
+
42
+ register_magic_typecaster
43
+ register_type_converter
44
+ end
45
+
46
+ def register_magic_typecaster
47
+ found_typecaster = Neo4j::Shared::TypeConverters.typecaster_for(options[:type])
48
+ return unless found_typecaster && found_typecaster.respond_to?(:primitive_type)
49
+ options[:typecaster] = found_typecaster
50
+ @magic_typecaster = options[:type]
51
+ options[:type] = found_typecaster.primitive_type
52
+ end
53
+
54
+ def register_type_converter
55
+ converter = options[:serializer]
56
+ return unless converter
57
+ options[:type] = converter.convert_type
58
+ options[:typecaster] = ActiveAttr::Typecasting::ObjectTypecaster.new
59
+ Neo4j::Shared::TypeConverters.register_converter(converter)
60
+ end
61
+ end
62
+ end