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