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
@@ -0,0 +1,150 @@
1
+ module Neo4j::Shared
2
+ # The DeclaredPropertyuManager holds details about objects created as a result of calling the #property
3
+ # class method on a class that includes Neo4j::ActiveNode or Neo4j::ActiveRel. There are many options
4
+ # that are referenced frequently, particularly during load and save, so this provides easy access and
5
+ # a way of separating behavior from the general Active{obj} modules.
6
+ #
7
+ # See Neo4j::Shared::DeclaredProperty for definitions of the property objects themselves.
8
+ class DeclaredPropertyManager
9
+ include Neo4j::Shared::TypeConverters
10
+
11
+ attr_reader :klass
12
+
13
+ # Each class that includes Neo4j::ActiveNode or Neo4j::ActiveRel gets one instance of this class.
14
+ # @param [#declared_property_manager] klass An object that has the #declared_property_manager method.
15
+ def initialize(klass)
16
+ @klass = klass
17
+ end
18
+
19
+ # @param [Neo4j::Shared::DeclaredProperty] property An instance of DeclaredProperty, created as the result of calling
20
+ # #property on an ActiveNode or ActiveRel class. The DeclaredProperty has specifics about the property, but registration
21
+ # makes the management object aware of it. This is necessary for type conversion, defaults, and inclusion in the nil and string hashes.
22
+ def register(property)
23
+ @_attributes_nil_hash = nil
24
+ @_attributes_string_map = nil
25
+ registered_properties[property.name] = property
26
+ register_magic_typecaster(property) if property.magic_typecaster
27
+ declared_property_defaults[property.name] = property.default_value if property.default_value
28
+ end
29
+
30
+ # The :default option in Neo4j::ActiveNode#property class method allows for setting a default value instead of
31
+ # nil on declared properties. This holds those values.
32
+ def declared_property_defaults
33
+ @_default_property_values ||= {}
34
+ end
35
+
36
+ def registered_properties
37
+ @_registered_properties ||= {}
38
+ end
39
+
40
+ # During object wrap, a hash is needed that contains each declared property with a nil value.
41
+ # The active_attr dependency is capable of providing this but it is expensive and calculated on the fly
42
+ # each time it is called. Rather than rely on that, we build this progressively as properties are registered.
43
+ # When the node or rel is loaded, this is used as a template.
44
+ def attributes_nil_hash
45
+ @_attributes_nil_hash ||= {}.tap do |attr_hash|
46
+ registered_properties.each_pair do |k, prop_obj|
47
+ val = prop_obj.default_value
48
+ attr_hash[k.to_s] = val
49
+ end
50
+ end.freeze
51
+ end
52
+
53
+ # During object wrapping, a props hash is built with string keys but Neo4j-core provides symbols.
54
+ # Rather than a `to_s` or `symbolize_keys` during every load, we build a map of symbol-to-string
55
+ # to speed up the process. This increases memory used by the gem but reduces object allocation and GC, so it is faster
56
+ # in practice.
57
+ def attributes_string_map
58
+ @_attributes_string_map ||= {}.tap do |attr_hash|
59
+ attributes_nil_hash.each_key { |k| attr_hash[k.to_sym] = k }
60
+ end.freeze
61
+ end
62
+
63
+ # @param [Symbol] k A symbol for which the String representation is sought. This might seem silly -- we could just call #to_s --
64
+ # but when this happens many times while loading many objects, it results in a surprisingly significant slowdown.
65
+ # The branching logic handles what happens if a property can't be found.
66
+ # The first option attempts to find it in the existing hash.
67
+ # The second option checks whether the key is the class's id property and, if it is, the string hash is rebuilt with it to prevent
68
+ # future lookups.
69
+ # The third calls `to_s`. This would happen if undeclared properties are found on the object. We could add them to the string map
70
+ # but that would result in unchecked, un-GCed memory consumption. In the event that someone is adding properties dynamically,
71
+ # maybe through user input, this would be bad.
72
+ def string_key(k)
73
+ attributes_string_map[k] || string_map_id_property(k) || k.to_s
74
+ end
75
+
76
+ def unregister(name)
77
+ # might need to be include?(name.to_s)
78
+ fail ArgumentError, "Argument `#{name}` not an attribute" if not registered_properties[name]
79
+ declared_prop = registered_properties[name]
80
+ registered_properties.delete(declared_prop)
81
+ unregister_magic_typecaster(name)
82
+ unregister_property_default(name)
83
+ end
84
+
85
+ def serialize(name, coder = JSON)
86
+ @serialize ||= {}
87
+ @serialize[name] = coder
88
+ end
89
+
90
+ def serialized_properties=(serialize_hash)
91
+ @serialized_property_keys = nil
92
+ @serialize = serialize_hash.clone
93
+ end
94
+
95
+ def serialized_properties
96
+ @serialize ||= {}
97
+ end
98
+
99
+ def serialized_properties_keys
100
+ @serialized_property_keys ||= serialized_properties.keys
101
+ end
102
+
103
+ def magic_typecast_properties_keys
104
+ @magic_typecast_properties_keys ||= magic_typecast_properties.keys
105
+ end
106
+
107
+ def magic_typecast_properties
108
+ @magic_typecast_properties ||= {}
109
+ end
110
+
111
+ # The known mappings of declared properties and their primitive types.
112
+ def upstream_primitives
113
+ @upstream_primitives ||= {}
114
+ end
115
+
116
+ protected
117
+
118
+ # Prevents repeated calls to :_attribute_type, which isn't free and never changes.
119
+ def fetch_upstream_primitive(attr)
120
+ upstream_primitives[attr] || upstream_primitives[attr] = klass._attribute_type(attr)
121
+ end
122
+
123
+ private
124
+
125
+ # @param [Symbol] key An undeclared property value found in the _persisted_obj.props hash.
126
+ # Typically, this is a node's id property, which will not be registered as other properties are.
127
+ # In the future, this should probably be reworked a bit. This class should either not know or care
128
+ # about the id property or it should be in charge of it. In the meantime, this improves
129
+ # node load performance.
130
+ def string_map_id_property(key)
131
+ return unless klass.id_property_name == key
132
+ @_attributes_string_map = attributes_string_map.dup.tap { |h| h[key] = key.to_s }.freeze
133
+ end
134
+
135
+ def unregister_magic_typecaster(property)
136
+ magic_typecast_properties.delete(property)
137
+ @magic_typecast_properties_keys = nil
138
+ end
139
+
140
+ def unregister_property_default(property)
141
+ declared_property_defaults.delete(property)
142
+ @_default_property_values = nil
143
+ end
144
+
145
+ def register_magic_typecaster(property)
146
+ magic_typecast_properties[property.name] = property.magic_typecaster
147
+ @magic_typecast_properties_keys = nil
148
+ end
149
+ end
150
+ end
@@ -1,17 +1,16 @@
1
1
  module Neo4j::Shared
2
2
  module Persistence
3
3
  extend ActiveSupport::Concern
4
- include Neo4j::Shared::TypeConverters
5
4
 
6
5
  USES_CLASSNAME = []
7
6
 
8
7
  def update_model
9
- if changed_attributes && !changed_attributes.empty?
10
- changed_props = attributes.select { |k, _| changed_attributes.include?(k) }
11
- changed_props = convert_properties_to :db, changed_props
12
- _persisted_obj.update_props(changed_props)
13
- changed_attributes.clear
14
- end
8
+ return if !changed_attributes || changed_attributes.empty?
9
+
10
+ changed_props = attributes.select { |k, _| changed_attributes.include?(k) }
11
+ changed_props = self.class.declared_property_manager.convert_properties_to(self, :db, changed_props)
12
+ _persisted_obj.update_props(changed_props)
13
+ changed_attributes.clear
15
14
  end
16
15
 
17
16
  # Convenience method to set attribute and #save at the same time
@@ -33,6 +32,7 @@ module Neo4j::Shared
33
32
  def create_or_update
34
33
  # since the same model can be created or updated twice from a relationship we have to have this guard
35
34
  @_create_or_updating = true
35
+ apply_default_values
36
36
  result = _persisted_obj ? update_model : create_model
37
37
  if result == false
38
38
  Neo4j::Transaction.current.failure if Neo4j::Transaction.current
@@ -47,6 +47,13 @@ module Neo4j::Shared
47
47
  @_create_or_updating = nil
48
48
  end
49
49
 
50
+ def apply_default_values
51
+ return if self.class.declared_property_defaults.empty?
52
+ self.class.declared_property_defaults.each_pair do |key, value|
53
+ self.send("#{key}=", value) if self.send(key).nil?
54
+ end
55
+ end
56
+
50
57
  # Returns +true+ if the record is persisted, i.e. it's not a new record and it was not destroyed
51
58
  def persisted?
52
59
  !new_record? && !destroyed?
@@ -92,7 +99,7 @@ module Neo4j::Shared
92
99
 
93
100
  def reload
94
101
  return self if new_record?
95
- clear_association_cache
102
+ association_proxy_cache.clear
96
103
  changed_attributes && changed_attributes.clear
97
104
  unless reload_from_database
98
105
  @_deleted = true
@@ -11,22 +11,21 @@ module Neo4j::Shared
11
11
 
12
12
  class UndefinedPropertyError < RuntimeError; end
13
13
  class MultiparameterAssignmentError < StandardError; end
14
- class IllegalPropertyError < StandardError; end
15
-
16
- ILLEGAL_PROPS = %w(from_node to_node start_node end_node)
14
+ # @_declared_property_manager = DeclaredPropertyManager.new
17
15
 
18
16
  attr_reader :_persisted_obj
19
17
 
20
- def initialize(attributes = {}, options = {})
21
- attributes = process_attributes(attributes)
18
+ # TODO: Remove the commented :super entirely once this code is part of a release.
19
+ # It calls an init method in active_attr that has a very negative impact on performance.
20
+ def initialize(attributes = {}, _options = nil)
21
+ attributes = process_attributes(attributes) unless attributes.empty?
22
22
  @relationship_props = self.class.extract_association_attributes!(attributes)
23
23
  writer_method_props = extract_writer_methods!(attributes)
24
24
  validate_attributes!(attributes)
25
- send_props(writer_method_props) unless writer_method_props.nil?
25
+ send_props(writer_method_props) unless writer_method_props.empty?
26
26
 
27
27
  @_persisted_obj = nil
28
-
29
- super(attributes, options)
28
+ # super(attributes, options)
30
29
  end
31
30
 
32
31
  # Returning nil when we get ActiveAttr::UnknownAttributeError from ActiveAttr
@@ -38,8 +37,8 @@ module Neo4j::Shared
38
37
  alias_method :[], :read_attribute
39
38
 
40
39
  def default_properties=(properties)
41
- keys = self.class.default_properties.keys
42
- @default_properties = properties.select { |key| keys.include?(key) }
40
+ default_property_keys = self.class.default_properties_keys
41
+ @default_properties = properties.select { |key| default_property_keys.include?(key) }
43
42
  end
44
43
 
45
44
  def default_property(key)
@@ -53,9 +52,17 @@ module Neo4j::Shared
53
52
  end
54
53
 
55
54
  def send_props(hash)
56
- hash.each do |key, value|
57
- self.send("#{key}=", value)
58
- end
55
+ hash.each { |key, value| self.send("#{key}=", value) }
56
+ end
57
+
58
+ protected
59
+
60
+ # This method is defined in ActiveModel.
61
+ # When each node is loaded, it is called once in pursuit of 'sanitize_for_mass_assignment', which this gem does not implement.
62
+ # In the course of doing that, it calls :attributes, which is quite expensive, so we return immediately.
63
+ def attribute_method?(attr_name) #:nodoc:
64
+ return false if attr_name == 'sanitize_for_mass_assignment'
65
+ super(attr_name)
59
66
  end
60
67
 
61
68
  private
@@ -63,13 +70,17 @@ module Neo4j::Shared
63
70
  # Changes attributes hash to remove relationship keys
64
71
  # Raises an error if there are any keys left which haven't been defined as properties on the model
65
72
  def validate_attributes!(attributes)
73
+ return attributes if attributes.empty?
66
74
  invalid_properties = attributes.keys.map(&:to_s) - self.attributes.keys
67
75
  fail UndefinedPropertyError, "Undefined properties: #{invalid_properties.join(',')}" if invalid_properties.size > 0
68
76
  end
69
77
 
70
78
  def extract_writer_methods!(attributes)
71
- attributes.keys.each_with_object({}) do |key, writer_method_props|
72
- writer_method_props[key] = attributes.delete(key) if self.respond_to?("#{key}=")
79
+ return attributes if attributes.empty?
80
+ {}.tap do |writer_method_props|
81
+ attributes.each_key do |key|
82
+ writer_method_props[key] = attributes.delete(key) if self.respond_to?("#{key}=")
83
+ end
73
84
  end
74
85
  end
75
86
 
@@ -78,8 +89,9 @@ module Neo4j::Shared
78
89
  multi_parameter_attributes = {}
79
90
  new_attributes = {}
80
91
  attributes.each_pair do |key, value|
81
- if key =~ /\A([^\(]+)\((\d+)([if])\)$/
82
- found_key, index = $1, $2.to_i
92
+ if match = key.match(/\A([^\(]+)\((\d+)([if])\)$/)
93
+ found_key = match[1]
94
+ index = match[2].to_i
83
95
  (multi_parameter_attributes[found_key] ||= {})[index] = value.empty? ? nil : value.send("to_#{$3}")
84
96
  else
85
97
  new_attributes[key] = value
@@ -90,16 +102,15 @@ module Neo4j::Shared
90
102
  end
91
103
 
92
104
  def process_multiparameter_attributes(multi_parameter_attributes, new_attributes)
93
- multi_parameter_attributes.each_pair do |key, values|
94
- begin
95
- values = (values.keys.min..values.keys.max).map { |i| values[i] }
96
- field = self.class.attributes[key.to_sym]
97
- new_attributes[key] = instantiate_object(field, values)
98
- rescue
99
- raise MultiparameterAssignmentError, "error on assignment #{values.inspect} to #{key}"
105
+ multi_parameter_attributes.each_with_object(new_attributes) do |(key, values), attributes|
106
+ values = (values.keys.min..values.keys.max).map { |i| values[i] }
107
+
108
+ if (field = self.class.attributes[key.to_sym]).nil?
109
+ fail MultiparameterAssignmentError, "error on assignment #{values.inspect} to #{key}"
100
110
  end
111
+
112
+ attributes[key] = instantiate_object(field, values)
101
113
  end
102
- new_attributes
103
114
  end
104
115
 
105
116
  def instantiate_object(field, values_with_empty_parameters)
@@ -110,6 +121,10 @@ module Neo4j::Shared
110
121
  end
111
122
 
112
123
  module ClassMethods
124
+ extend Forwardable
125
+
126
+ def_delegators :declared_property_manager, :serialized_properties, :serialized_properties=, :serialize, :declared_property_defaults
127
+
113
128
  # Defines a property on the class
114
129
  #
115
130
  # See active_attr gem for allowed options, e.g which type
@@ -139,22 +154,25 @@ module Neo4j::Shared
139
154
  # property :name, constraint: :unique
140
155
  # end
141
156
  def property(name, options = {})
142
- check_illegal_prop(name)
143
- magic_properties(name, options)
144
- attribute(name, options)
157
+ prop = DeclaredProperty.new(name, options)
158
+ prop.register
159
+ declared_property_manager.register(prop)
160
+
161
+ attribute(name, prop.options)
145
162
  constraint_or_index(name, options)
146
163
  end
147
164
 
148
165
  def undef_property(name)
149
- fail ArgumentError, "Argument `#{name}` not an attribute" if not attribute_names.include?(name.to_s)
150
-
151
- attribute_methods(name).each do |method|
152
- undef_method(method)
153
- end
154
-
166
+ declared_property_manager.unregister(name)
167
+ attribute_methods(name).each { |method| undef_method(method) }
155
168
  undef_constraint_or_index(name)
156
169
  end
157
170
 
171
+ def declared_property_manager
172
+ @_declared_property_manager ||= DeclaredPropertyManager.new(self)
173
+ end
174
+
175
+ # TODO: Move this to the DeclaredPropertyManager
158
176
  def default_property(name, &block)
159
177
  reset_default_properties(name) if default_properties.respond_to?(:size)
160
178
  default_properties[name] = block
@@ -165,10 +183,16 @@ module Neo4j::Shared
165
183
  @default_property ||= {}
166
184
  end
167
185
 
186
+ def default_properties_keys
187
+ @default_properties_keys ||= default_properties.keys
188
+ end
189
+
168
190
  def reset_default_properties(name_to_keep)
169
191
  default_properties.each_key do |property|
192
+ @default_properties_keys = nil
170
193
  undef_method(property) unless property == name_to_keep
171
194
  end
195
+ @default_properties_keys = nil
172
196
  @default_property = {}
173
197
  end
174
198
 
@@ -187,6 +211,12 @@ module Neo4j::Shared
187
211
  end
188
212
  end
189
213
 
214
+ # @return [Hash] A frozen hash of all model properties with nil values. It is used during node loading and prevents
215
+ # an extra call to a slow dependency method.
216
+ def attributes_nil_hash
217
+ declared_property_manager.attributes_nil_hash
218
+ end
219
+
190
220
  private
191
221
 
192
222
  def constraint_or_index(name, options)
@@ -199,21 +229,6 @@ module Neo4j::Shared
199
229
  index(name, options) if options[:index] == :exact
200
230
  end
201
231
  end
202
-
203
- def check_illegal_prop(name)
204
- if ILLEGAL_PROPS.include?(name.to_s)
205
- fail IllegalPropertyError, "#{name} is an illegal property"
206
- end
207
- end
208
-
209
- # Tweaks properties
210
- def magic_properties(name, options)
211
- options[:type] ||= DateTime if name.to_sym == :created_at || name.to_sym == :updated_at
212
-
213
- # ActiveAttr does not handle "Time", Rails and Neo4j.rb 2.3 did
214
- # Convert it to DateTime in the interest of consistency
215
- options[:type] = DateTime if options[:type] == Time
216
- end
217
232
  end
218
233
  end
219
234
  end
@@ -24,18 +24,19 @@ module Neo4j::Shared
24
24
  # @return [String] A string that conforms to the set rel type conversion setting.
25
25
  def decorated_rel_type(type)
26
26
  type = type.to_s
27
- case rel_transformer
28
- when :upcase
29
- type.underscore.upcase
30
- when :downcase
31
- type.underscore.downcase
32
- when :legacy
33
- "##{type.underscore.downcase}"
34
- when :none
35
- type
36
- else
37
- type.underscore.upcase
38
- end
27
+ decorated_type = case rel_transformer
28
+ when :upcase
29
+ type.underscore.upcase
30
+ when :downcase
31
+ type.underscore.downcase
32
+ when :legacy
33
+ "##{type.underscore.downcase}"
34
+ when :none
35
+ type
36
+ else
37
+ type.underscore.upcase
38
+ end
39
+ decorated_type.tap { |s| s.gsub!('/', '::') if type.include?('::') }
39
40
  end
40
41
  end
41
42
  end
@@ -14,20 +14,5 @@ module Neo4j::Shared
14
14
  def serializable_hash(*args)
15
15
  super.merge(id: id)
16
16
  end
17
-
18
- module ClassMethods
19
- def serialized_properties
20
- @serialize || {}
21
- end
22
-
23
- def serialized_properties=(serialize_hash)
24
- @serialize = serialize_hash.clone
25
- end
26
-
27
- def serialize(name, coder = JSON)
28
- @serialize ||= {}
29
- @serialize[name] = coder
30
- end
31
- end
32
17
  end
33
18
  end