neo4j 4.1.5 → 5.0.0.rc.1

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