neo4j_legacy 7.2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1357 -0
  3. data/CONTRIBUTORS +8 -0
  4. data/Gemfile +38 -0
  5. data/README.md +103 -0
  6. data/bin/neo4j-jars +33 -0
  7. data/bin/rake +17 -0
  8. data/config/locales/en.yml +5 -0
  9. data/config/neo4j/add_classnames.yml +1 -0
  10. data/config/neo4j/config.yml +35 -0
  11. data/lib/active_support/per_thread_registry.rb +1 -0
  12. data/lib/backports/action_controller/metal/strong_parameters.rb +672 -0
  13. data/lib/backports/active_model/forbidden_attributes_protection.rb +30 -0
  14. data/lib/backports/active_support/concern.rb +13 -0
  15. data/lib/backports/active_support/core_ext/module/attribute_accessors.rb +10 -0
  16. data/lib/backports/active_support/logger.rb +99 -0
  17. data/lib/backports/active_support/logger_silence.rb +27 -0
  18. data/lib/backports/active_support/logger_thread_safe_level.rb +32 -0
  19. data/lib/backports/active_support/per_thread_registry.rb +60 -0
  20. data/lib/backports.rb +4 -0
  21. data/lib/neo4j/active_node/callbacks.rb +8 -0
  22. data/lib/neo4j/active_node/dependent/association_methods.rb +48 -0
  23. data/lib/neo4j/active_node/dependent/query_proxy_methods.rb +50 -0
  24. data/lib/neo4j/active_node/dependent.rb +11 -0
  25. data/lib/neo4j/active_node/enum.rb +29 -0
  26. data/lib/neo4j/active_node/has_n/association/rel_factory.rb +61 -0
  27. data/lib/neo4j/active_node/has_n/association/rel_wrapper.rb +23 -0
  28. data/lib/neo4j/active_node/has_n/association.rb +280 -0
  29. data/lib/neo4j/active_node/has_n/association_cypher_methods.rb +108 -0
  30. data/lib/neo4j/active_node/has_n.rb +532 -0
  31. data/lib/neo4j/active_node/id_property/accessor.rb +62 -0
  32. data/lib/neo4j/active_node/id_property.rb +187 -0
  33. data/lib/neo4j/active_node/initialize.rb +21 -0
  34. data/lib/neo4j/active_node/labels/index.rb +87 -0
  35. data/lib/neo4j/active_node/labels/reloading.rb +21 -0
  36. data/lib/neo4j/active_node/labels.rb +198 -0
  37. data/lib/neo4j/active_node/node_wrapper.rb +52 -0
  38. data/lib/neo4j/active_node/orm_adapter.rb +82 -0
  39. data/lib/neo4j/active_node/persistence.rb +175 -0
  40. data/lib/neo4j/active_node/property.rb +60 -0
  41. data/lib/neo4j/active_node/query/query_proxy.rb +361 -0
  42. data/lib/neo4j/active_node/query/query_proxy_eager_loading.rb +61 -0
  43. data/lib/neo4j/active_node/query/query_proxy_enumerable.rb +90 -0
  44. data/lib/neo4j/active_node/query/query_proxy_find_in_batches.rb +19 -0
  45. data/lib/neo4j/active_node/query/query_proxy_link.rb +117 -0
  46. data/lib/neo4j/active_node/query/query_proxy_methods.rb +210 -0
  47. data/lib/neo4j/active_node/query/query_proxy_methods_of_mass_updating.rb +83 -0
  48. data/lib/neo4j/active_node/query.rb +76 -0
  49. data/lib/neo4j/active_node/query_methods.rb +65 -0
  50. data/lib/neo4j/active_node/reflection.rb +86 -0
  51. data/lib/neo4j/active_node/rels.rb +11 -0
  52. data/lib/neo4j/active_node/scope.rb +146 -0
  53. data/lib/neo4j/active_node/unpersisted.rb +48 -0
  54. data/lib/neo4j/active_node/validations.rb +59 -0
  55. data/lib/neo4j/active_node.rb +105 -0
  56. data/lib/neo4j/active_rel/callbacks.rb +15 -0
  57. data/lib/neo4j/active_rel/initialize.rb +28 -0
  58. data/lib/neo4j/active_rel/persistence/query_factory.rb +95 -0
  59. data/lib/neo4j/active_rel/persistence.rb +114 -0
  60. data/lib/neo4j/active_rel/property.rb +95 -0
  61. data/lib/neo4j/active_rel/query.rb +95 -0
  62. data/lib/neo4j/active_rel/rel_wrapper.rb +22 -0
  63. data/lib/neo4j/active_rel/related_node.rb +83 -0
  64. data/lib/neo4j/active_rel/types.rb +82 -0
  65. data/lib/neo4j/active_rel/validations.rb +8 -0
  66. data/lib/neo4j/active_rel.rb +67 -0
  67. data/lib/neo4j/class_arguments.rb +39 -0
  68. data/lib/neo4j/config.rb +124 -0
  69. data/lib/neo4j/core/query.rb +22 -0
  70. data/lib/neo4j/errors.rb +28 -0
  71. data/lib/neo4j/migration.rb +127 -0
  72. data/lib/neo4j/paginated.rb +27 -0
  73. data/lib/neo4j/railtie.rb +169 -0
  74. data/lib/neo4j/schema/operation.rb +91 -0
  75. data/lib/neo4j/shared/attributes.rb +220 -0
  76. data/lib/neo4j/shared/callbacks.rb +64 -0
  77. data/lib/neo4j/shared/cypher.rb +37 -0
  78. data/lib/neo4j/shared/declared_properties.rb +204 -0
  79. data/lib/neo4j/shared/declared_property/index.rb +37 -0
  80. data/lib/neo4j/shared/declared_property.rb +118 -0
  81. data/lib/neo4j/shared/enum.rb +148 -0
  82. data/lib/neo4j/shared/filtered_hash.rb +79 -0
  83. data/lib/neo4j/shared/identity.rb +28 -0
  84. data/lib/neo4j/shared/initialize.rb +28 -0
  85. data/lib/neo4j/shared/marshal.rb +23 -0
  86. data/lib/neo4j/shared/mass_assignment.rb +58 -0
  87. data/lib/neo4j/shared/permitted_attributes.rb +28 -0
  88. data/lib/neo4j/shared/persistence.rb +231 -0
  89. data/lib/neo4j/shared/property.rb +220 -0
  90. data/lib/neo4j/shared/query_factory.rb +101 -0
  91. data/lib/neo4j/shared/rel_type_converters.rb +43 -0
  92. data/lib/neo4j/shared/serialized_properties.rb +30 -0
  93. data/lib/neo4j/shared/type_converters.rb +418 -0
  94. data/lib/neo4j/shared/typecasted_attributes.rb +98 -0
  95. data/lib/neo4j/shared/typecaster.rb +53 -0
  96. data/lib/neo4j/shared/validations.rb +48 -0
  97. data/lib/neo4j/shared.rb +51 -0
  98. data/lib/neo4j/tasks/migration.rake +24 -0
  99. data/lib/neo4j/timestamps/created.rb +9 -0
  100. data/lib/neo4j/timestamps/updated.rb +9 -0
  101. data/lib/neo4j/timestamps.rb +11 -0
  102. data/lib/neo4j/type_converters.rb +7 -0
  103. data/lib/neo4j/version.rb +3 -0
  104. data/lib/neo4j/wrapper.rb +4 -0
  105. data/lib/neo4j.rb +96 -0
  106. data/lib/rails/generators/neo4j/model/model_generator.rb +86 -0
  107. data/lib/rails/generators/neo4j/model/templates/model.erb +15 -0
  108. data/lib/rails/generators/neo4j_generator.rb +67 -0
  109. data/neo4j.gemspec +43 -0
  110. metadata +389 -0
@@ -0,0 +1,90 @@
1
+ module Neo4j
2
+ module ActiveNode
3
+ module Query
4
+ # Methods related to returning nodes and rels from QueryProxy
5
+ module QueryProxyEnumerable
6
+ include Enumerable
7
+
8
+ # Just like every other <tt>each</tt> but it allows for optional params to support the versions that also return relationships.
9
+ # The <tt>node</tt> and <tt>rel</tt> params are typically used by those other methods but there's nothing stopping you from
10
+ # using `your_node.each(true, true)` instead of `your_node.each_with_rel`.
11
+ # @return [Enumerable] An enumerable containing some combination of nodes and rels.
12
+ def each(node = true, rel = nil, &block)
13
+ result(node, rel).each(&block)
14
+ end
15
+
16
+ def result(node = true, rel = nil)
17
+ @result_cache ||= {}
18
+ return result_cache_for(node, rel) if result_cache?(node, rel)
19
+
20
+ pluck_vars = []
21
+ pluck_vars << identity if node
22
+ pluck_vars << @rel_var if rel
23
+
24
+ result = pluck(*pluck_vars)
25
+
26
+ result.each do |object|
27
+ object.instance_variable_set('@source_query_proxy', self)
28
+ object.instance_variable_set('@source_proxy_result_cache', result)
29
+ end
30
+
31
+ @result_cache[[node, rel]] ||= result
32
+ end
33
+
34
+ def result_cache?(node = true, rel = nil)
35
+ !!result_cache_for(node, rel)
36
+ end
37
+
38
+ def result_cache_for(node = true, rel = nil)
39
+ (@result_cache || {})[[node, rel]]
40
+ end
41
+
42
+ def fetch_result_cache
43
+ @result_cache ||= yield
44
+ end
45
+
46
+ # When called at the end of a QueryProxy chain, it will return the resultant relationship objects intead of nodes.
47
+ # For example, to return the relationship between a given student and their lessons:
48
+ #
49
+ # .. code-block:: ruby
50
+ #
51
+ # student.lessons.each_rel do |rel|
52
+ #
53
+ # @return [Enumerable] An enumerable containing any number of applicable relationship objects.
54
+ def each_rel(&block)
55
+ block_given? ? each(false, true, &block) : to_enum(:each, false, true)
56
+ end
57
+
58
+ # When called at the end of a QueryProxy chain, it will return the nodes and relationships of the last link.
59
+ # For example, to return a lesson and each relationship to a given student:
60
+ #
61
+ # .. code-block:: ruby
62
+ #
63
+ # student.lessons.each_with_rel do |lesson, rel|
64
+ def each_with_rel(&block)
65
+ block_given? ? each(true, true, &block) : to_enum(:each, true, true)
66
+ end
67
+
68
+ # Does exactly what you would hope. Without it, comparing `bobby.lessons == sandy.lessons` would evaluate to false because it
69
+ # would be comparing the QueryProxy objects, not the lessons themselves.
70
+ def ==(other)
71
+ self.to_a == other
72
+ end
73
+
74
+ # For getting variables which have been defined as part of the association chain
75
+ def pluck(*args)
76
+ transformable_attributes = (model ? model.attribute_names : []) + %w(uuid neo_id)
77
+ arg_list = args.map do |arg|
78
+ if transformable_attributes.include?(arg.to_s)
79
+ {identity => arg}
80
+ else
81
+ arg
82
+ end
83
+ end
84
+
85
+ self.query.pluck(*arg_list)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,19 @@
1
+ module Neo4j
2
+ module ActiveNode
3
+ module Query
4
+ module QueryProxyFindInBatches
5
+ def find_in_batches(options = {})
6
+ query.return(identity).find_in_batches(identity, @model.primary_key, options) do |batch|
7
+ yield batch.map(&identity)
8
+ end
9
+ end
10
+
11
+ def find_each(options = {})
12
+ query.return(identity).find_each(identity, @model.primary_key, options) do |result|
13
+ yield result.send(identity)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,117 @@
1
+ module Neo4j
2
+ module ActiveNode
3
+ module Query
4
+ class QueryProxy
5
+ class Link
6
+ attr_reader :clause
7
+
8
+ def initialize(clause, arg, args = [])
9
+ @clause = clause
10
+ @arg = arg
11
+ @args = args
12
+ end
13
+
14
+ def args(var, rel_var)
15
+ @arg.respond_to?(:call) ? @arg.call(var, rel_var) : [@arg, @args].flatten
16
+ end
17
+
18
+ class << self
19
+ def for_clause(clause, arg, model, *args)
20
+ method_to_call = "for_#{clause}_clause"
21
+
22
+ send(method_to_call, arg, model, *args)
23
+ end
24
+
25
+ def for_where_clause(arg, model, *args)
26
+ node_num = 1
27
+ result = []
28
+ if arg.is_a?(Hash)
29
+ arg.each do |key, value|
30
+ if model && model.association?(key)
31
+ result += for_association(key, value, "n#{node_num}", model)
32
+ node_num += 1
33
+ else
34
+ result << new_for_key_and_value(model, key, value)
35
+ end
36
+ end
37
+ elsif arg.is_a?(String)
38
+ result << new(:where, arg, args)
39
+ end
40
+ result
41
+ end
42
+ alias_method :for_node_where_clause, :for_where_clause
43
+
44
+ def for_where_not_clause(*args)
45
+ for_where_clause(*args).each do |link|
46
+ link.instance_variable_set('@clause', :where_not)
47
+ end
48
+ end
49
+
50
+ def new_for_key_and_value(model, key, value)
51
+ key = (key.to_sym == :id ? model.id_property_name : key)
52
+
53
+ val = if !model
54
+ value
55
+ elsif key == model.id_property_name && value.is_a?(Neo4j::ActiveNode)
56
+ value.id
57
+ else
58
+ converted_value(model, key, value)
59
+ end
60
+
61
+ new(:where, ->(v, _) { {v => {key => val}} })
62
+ end
63
+
64
+ def for_association(name, value, n_string, model)
65
+ neo_id = value.try(:neo_id) || value
66
+ fail ArgumentError, "Invalid value for '#{name}' condition" if not neo_id.is_a?(Integer)
67
+
68
+ [
69
+ new(:match, ->(v, _) { "(#{v})#{model.associations[name].arrow_cypher}(#{n_string})" }),
70
+ new(:where, ->(_, _) { {"ID(#{n_string})" => neo_id.to_i} })
71
+ ]
72
+ end
73
+
74
+ # We don't accept strings here. If you want to use a string, just use where.
75
+ def for_rel_where_clause(arg, _, association)
76
+ arg.each_with_object([]) do |(key, value), result|
77
+ rel_class = association.relationship_class if association.relationship_class
78
+ val = rel_class ? converted_value(rel_class, key, value) : value
79
+ result << new(:where, ->(_, rel_var) { {rel_var => {key => val}} })
80
+ end
81
+ end
82
+
83
+ def for_rel_order_clause(arg, _)
84
+ [new(:order, ->(_, v) { arg.is_a?(String) ? arg : {v => arg} })]
85
+ end
86
+
87
+ def for_order_clause(arg, _)
88
+ [new(:order, ->(v, _) { arg.is_a?(String) ? arg : {v => arg} })]
89
+ end
90
+
91
+ def for_args(model, clause, args, association = nil)
92
+ if [:where, :where_not].include?(clause) && args[0].is_a?(String) # Better way?
93
+ [for_arg(model, clause, args[0], *args[1..-1])]
94
+ elsif clause == :rel_where
95
+ args.map { |arg| for_arg(model, clause, arg, association) }
96
+ else
97
+ args.map { |arg| for_arg(model, clause, arg) }
98
+ end
99
+ end
100
+
101
+ def for_arg(model, clause, arg, *args)
102
+ default = [Link.new(clause, arg, *args)]
103
+
104
+ Link.for_clause(clause, arg, model, *args) || default
105
+ rescue NoMethodError
106
+ default
107
+ end
108
+
109
+ def converted_value(model, key, value)
110
+ model.declared_properties.value_for_where(key, value)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,210 @@
1
+ module Neo4j
2
+ module ActiveNode
3
+ module Query
4
+ module QueryProxyMethods
5
+ FIRST = 'HEAD'
6
+ LAST = 'LAST'
7
+
8
+ def rels
9
+ fail 'Cannot get rels without a relationship variable.' if !@rel_var
10
+
11
+ pluck(@rel_var)
12
+ end
13
+
14
+ def rel
15
+ rels.first
16
+ end
17
+
18
+ # Give ability to call `#find` on associations to get a scoped find
19
+ # Doesn't pass through via `method_missing` because Enumerable has a `#find` method
20
+ def find(*args)
21
+ scoping { @model.find(*args) }
22
+ end
23
+
24
+ def first(target = nil)
25
+ first_and_last(FIRST, target)
26
+ end
27
+
28
+ def last(target = nil)
29
+ first_and_last(LAST, target)
30
+ end
31
+
32
+ def order_property
33
+ # This should maybe be based on a setting in the association
34
+ # rather than a hardcoded `nil`
35
+ model ? model.id_property_name : nil
36
+ end
37
+
38
+ # @return [Integer] number of nodes of this class
39
+ def count(distinct = nil, target = nil)
40
+ fail(Neo4j::InvalidParameterError, ':count accepts `distinct` or nil as a parameter') unless distinct.nil? || distinct == :distinct
41
+ query_with_target(target) do |var|
42
+ q = distinct.nil? ? var : "DISTINCT #{var}"
43
+ limited_query = self.query.clause?(:limit) ? self.query.break.with(var) : self.query.reorder
44
+ limited_query.pluck("count(#{q}) AS #{var}").first
45
+ end
46
+ end
47
+
48
+ def size
49
+ result_cache? ? result_cache_for.length : count
50
+ end
51
+
52
+ delegate :length, to: :to_a
53
+
54
+ # TODO: update this with public API methods if/when they are exposed
55
+ def limit_value
56
+ return unless self.query.clause?(:limit)
57
+ limit_clause = self.query.send(:clauses).find { |clause| clause.is_a?(Neo4j::Core::QueryClauses::LimitClause) }
58
+ limit_clause.instance_variable_get(:@arg)
59
+ end
60
+
61
+ def empty?(target = nil)
62
+ query_with_target(target) { |var| !self.exists?(nil, var) }
63
+ end
64
+
65
+ alias_method :blank?, :empty?
66
+
67
+ # @param [Neo4j::ActiveNode, Neo4j::Node, String] other An instance of a Neo4j.rb model, a Neo4j-core node, or a string uuid
68
+ # @param [String, Symbol] target An identifier of a link in the Cypher chain
69
+ # @return [Boolean]
70
+ def include?(other, target = nil)
71
+ query_with_target(target) do |var|
72
+ where_filter = if other.respond_to?(:neo_id)
73
+ "ID(#{var}) = {other_node_id}"
74
+ else
75
+ "#{var}.#{association_id_key} = {other_node_id}"
76
+ end
77
+ node_id = other.respond_to?(:neo_id) ? other.neo_id : other
78
+ self.where(where_filter).params(other_node_id: node_id).query.reorder.return("count(#{var}) as count").first.count > 0
79
+ end
80
+ end
81
+
82
+ def exists?(node_condition = nil, target = nil)
83
+ unless node_condition.is_a?(Integer) || node_condition.is_a?(Hash) || node_condition.nil?
84
+ fail(Neo4j::InvalidParameterError, ':exists? only accepts neo_ids')
85
+ end
86
+ query_with_target(target) do |var|
87
+ start_q = exists_query_start(node_condition, var)
88
+ start_q.query.reorder.return("COUNT(#{var}) AS count").first.count > 0
89
+ end
90
+ end
91
+
92
+ # Shorthand for `MATCH (start)-[r]-(other_node) WHERE ID(other_node) = #{other_node.neo_id}`
93
+ # The `node` param can be a persisted ActiveNode instance, any string or integer, or nil.
94
+ # When it's a node, it'll use the object's neo_id, which is fastest. When not nil, it'll figure out the
95
+ # primary key of that model. When nil, it uses `1 = 2` to prevent matching all records, which is the default
96
+ # behavior when nil is passed to `where` in QueryProxy.
97
+ # @param [#neo_id, String, Enumerable] node A node, a string representing a node's ID, or an enumerable of nodes or IDs.
98
+ # @return [Neo4j::ActiveNode::Query::QueryProxy] A QueryProxy object upon which you can build.
99
+ def match_to(node)
100
+ first_node = node.is_a?(Array) ? node.first : node
101
+ where_arg = if first_node.respond_to?(:neo_id)
102
+ {neo_id: node.is_a?(Array) ? node.map(&:neo_id) : node}
103
+ elsif !node.nil?
104
+ {association_id_key => node.is_a?(Array) ? ids_array(node) : node}
105
+ else
106
+ # support for null object pattern
107
+ '1 = 2'
108
+ end
109
+
110
+ self.where(where_arg)
111
+ end
112
+
113
+
114
+ # Gives you the first relationship between the last link of a QueryProxy chain and a given node
115
+ # Shorthand for `MATCH (start)-[r]-(other_node) WHERE ID(other_node) = #{other_node.neo_id} RETURN r`
116
+ # @param [#neo_id, String, Enumerable] node An object to be sent to `match_to`. See params for that method.
117
+ # @return A relationship (ActiveRel, CypherRelationship, EmbeddedRelationship) or nil.
118
+ def first_rel_to(node)
119
+ self.match_to(node).limit(1).pluck(rel_var).first
120
+ end
121
+
122
+ # Returns all relationships across a QueryProxy chain between a given node or array of nodes and the preceeding link.
123
+ # @param [#neo_id, String, Enumerable] node An object to be sent to `match_to`. See params for that method.
124
+ # @return An enumerable of relationship objects.
125
+ def rels_to(node)
126
+ self.match_to(node).pluck(rel_var)
127
+ end
128
+ alias_method :all_rels_to, :rels_to
129
+
130
+ # When called, this method returns a single node that satisfies the match specified in the params hash.
131
+ # If no existing node is found to satisfy the match, one is created or associated as expected.
132
+ def find_or_create_by(params)
133
+ fail 'Method invalid when called on Class objects' unless source_object
134
+ result = self.where(params).first
135
+ return result unless result.nil?
136
+ Neo4j::Transaction.run do
137
+ node = model.find_or_create_by(params)
138
+ self << node
139
+ return node
140
+ end
141
+ end
142
+
143
+ # A shortcut for attaching a new, optional match to the end of a QueryProxy chain.
144
+ def optional(association, node_var = nil, rel_var = nil)
145
+ self.send(association, node_var, rel_var, optional: true)
146
+ end
147
+
148
+ # Takes an Array of ActiveNode models and applies the appropriate WHERE clause
149
+ # So for a `Teacher` model inheriting from a `Person` model and an `Article` model
150
+ # if you called .as_models([Teacher, Article])
151
+ # The where clause would look something like:
152
+ #
153
+ # .. code-block:: cypher
154
+ #
155
+ # WHERE (node_var:Teacher:Person OR node_var:Article)
156
+ def as_models(models)
157
+ where_clause = models.map do |model|
158
+ "`#{identity}`:" + model.mapped_label_names.map do |mapped_label_name|
159
+ "`#{mapped_label_name}`"
160
+ end.join(':')
161
+ end.join(' OR ')
162
+
163
+ where("(#{where_clause})")
164
+ end
165
+
166
+ private
167
+
168
+ def first_and_last(func, target)
169
+ new_query, pluck_proc = if self.query.clause?(:order)
170
+ [self.query.with(identity),
171
+ proc { |var| "#{func}(COLLECT(#{var})) as #{var}" }]
172
+ else
173
+ [self.order(order_property).limit(1),
174
+ proc { |var| var }]
175
+ end
176
+ query_with_target(target) do |var|
177
+ final_pluck = pluck_proc.call(var)
178
+ new_query.pluck(final_pluck)
179
+ end.first
180
+ end
181
+
182
+ # @return [String] The primary key of a the current QueryProxy's model or target class
183
+ def association_id_key
184
+ self.association.nil? ? model.primary_key : self.association.target_class.primary_key
185
+ end
186
+
187
+ # @param [Enumerable] node An enumerable of nodes or ids.
188
+ # @return [Array] An array after having `id` called on each object
189
+ def ids_array(node)
190
+ node.first.respond_to?(:id) ? node.map(&:id) : node
191
+ end
192
+
193
+ def query_with_target(target)
194
+ yield(target || identity)
195
+ end
196
+
197
+ def exists_query_start(condition, target)
198
+ case condition
199
+ when Integer
200
+ self.where("ID(#{target}) = {exists_condition}").params(exists_condition: condition)
201
+ when Hash
202
+ self.where(condition.keys.first => condition.values.first)
203
+ else
204
+ self
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,83 @@
1
+ module Neo4j
2
+ module ActiveNode
3
+ module Query
4
+ module QueryProxyMethodsOfMassUpdating
5
+ # Updates some attributes of a group of nodes within a QP chain.
6
+ # The optional argument makes sense only of `updates` is a string.
7
+ # @param [Hash,String] updates An hash or a string of parameters to be updated.
8
+ # @param [Hash] params An hash of parameters for the update string. It's ignored if `updates` is an Hash.
9
+ def update_all(updates, params = {})
10
+ # Move this to ActiveNode module?
11
+ update_all_with_query(identity, updates, params)
12
+ end
13
+
14
+ # Updates some attributes of a group of relationships within a QP chain.
15
+ # The optional argument makes sense only of `updates` is a string.
16
+ # @param [Hash,String] updates An hash or a string of parameters to be updated.
17
+ # @param [Hash] params An hash of parameters for the update string. It's ignored if `updates` is an Hash.
18
+ def update_all_rels(updates, params = {})
19
+ fail 'Cannot update rels without a relationship variable.' unless @rel_var
20
+ update_all_with_query(@rel_var, updates, params)
21
+ end
22
+
23
+ # Deletes a group of nodes and relationships within a QP chain. When identifier is omitted, it will remove the last link in the chain.
24
+ # The optional argument must be a node identifier. A relationship identifier will result in a Cypher Error
25
+ # @param identifier [String,Symbol] the optional identifier of the link in the chain to delete.
26
+ def delete_all(identifier = nil)
27
+ query_with_target(identifier) do |target|
28
+ begin
29
+ self.query.with(target).optional_match("(#{target})-[#{target}_rel]-()").delete("#{target}, #{target}_rel").exec
30
+ rescue Neo4j::Session::CypherError
31
+ self.query.delete(target).exec
32
+ end
33
+ clear_source_object_cache
34
+ end
35
+ end
36
+
37
+ # Deletes the relationship between a node and its last link in the QueryProxy chain. Executed in the database, callbacks will not run.
38
+ def delete(node)
39
+ self.match_to(node).query.delete(rel_var).exec
40
+ clear_source_object_cache
41
+ end
42
+
43
+ # Deletes the relationships between all nodes for the last step in the QueryProxy chain. Executed in the database, callbacks will not be run.
44
+ def delete_all_rels
45
+ return unless start_object && start_object._persisted_obj
46
+ self.query.delete(rel_var).exec
47
+ end
48
+
49
+ # Deletes the relationships between all nodes for the last step in the QueryProxy chain and replaces them with relationships to the given nodes.
50
+ # Executed in the database, callbacks will not be run.
51
+ def replace_with(node_or_nodes)
52
+ nodes = Array(node_or_nodes)
53
+
54
+ self.delete_all_rels
55
+ nodes.each { |node| self << node }
56
+ end
57
+
58
+ # Returns all relationships between a node and its last link in the QueryProxy chain, destroys them in Ruby. Callbacks will be run.
59
+ def destroy(node)
60
+ self.rels_to(node).map!(&:destroy)
61
+ clear_source_object_cache
62
+ end
63
+
64
+ private
65
+
66
+ def update_all_with_query(var_name, updates, params)
67
+ query = all.query
68
+
69
+ case updates
70
+ when Hash then query.set(var_name => updates).pluck("count(#{var_name})").first
71
+ when String then query.set(updates).params(params).pluck("count(#{var_name})").first
72
+ else
73
+ fail ArgumentError, "Invalid parameter type #{updates.class} for `updates`."
74
+ end
75
+ end
76
+
77
+ def clear_source_object_cache
78
+ self.source_object.clear_association_cache if self.source_object.respond_to?(:clear_association_cache)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,76 @@
1
+ module Neo4j
2
+ module ActiveNode
3
+ # Helper methods to return Neo4j::Core::Query objects. A query object can be used to successively build a cypher query
4
+ #
5
+ # person.query_as(:n).match('n-[:friend]-o').return(o: :name) # Return the names of all the person's friends
6
+ #
7
+ module Query
8
+ extend ActiveSupport::Concern
9
+
10
+ # Returns a Query object with the current node matched the specified variable name
11
+ #
12
+ # @example Return the names of all of Mike's friends
13
+ # # Generates: MATCH (mike:Person), mike-[:friend]-friend WHERE ID(mike) = 123 RETURN friend.name
14
+ # mike.query_as(:mike).match('mike-[:friend]-friend').return(friend: :name)
15
+ #
16
+ # @param node_var [Symbol, String] The variable name to specify in the query
17
+ # @return [Neo4j::Core::Query]
18
+ def query_as(node_var)
19
+ self.class.query_as(node_var, false).where("ID(#{node_var})" => self.neo_id)
20
+ end
21
+
22
+ # Starts a new QueryProxy with the starting identifier set to the given argument and QueryProxy source_object set to the node instance.
23
+ # This method does not exist within QueryProxy and can only be used to start a new chain.
24
+ #
25
+ # @example Start a new QueryProxy chain with the first identifier set manually
26
+ # # Generates: MATCH (s:`Student`), (l:`Lesson`), s-[rel1:`ENROLLED_IN`]->(l:`Lesson`) WHERE ID(s) = {neo_id_17963}
27
+ # student.as(:s).lessons(:l)
28
+ #
29
+ # @param [String, Symbol] node_var The identifier to use within the QueryProxy object
30
+ # @return [Neo4j::ActiveNode::Query::QueryProxy]
31
+ def as(node_var)
32
+ self.class.query_proxy(node: node_var, source_object: self).match_to(self)
33
+ end
34
+
35
+ module ClassMethods
36
+ # Returns a Query object with all nodes for the model matched as the specified variable name
37
+ #
38
+ # @example Return the registration number of all cars owned by a person over the age of 30
39
+ # # Generates: MATCH (person:Person), person-[:owned]-car WHERE person.age > 30 RETURN car.registration_number
40
+ # Person.query_as(:person).where('person.age > 30').match('person-[:owned]-car').return(car: :registration_number)
41
+ #
42
+ # @param [Symbol, String] var The variable name to specify in the query
43
+ # @param [Boolean] with_labels Should labels be used to build the match? There are situations (neo_id used to filter,
44
+ # an early Cypher match has already filtered results) where including labels will degrade performance.
45
+ # @return [Neo4j::Core::Query]
46
+ def query_as(var, with_labels = true)
47
+ query_proxy.query_as(var, with_labels)
48
+ end
49
+
50
+ Neo4j::ActiveNode::Query::QueryProxy::METHODS.each do |method|
51
+ define_method(method) do |*args|
52
+ self.query_proxy.send(method, *args)
53
+ end
54
+ end
55
+
56
+ def query_proxy(options = {})
57
+ Neo4j::ActiveNode::Query::QueryProxy.new(self, nil, options)
58
+ end
59
+
60
+ # Start a new QueryProxy with the starting identifier set to the given argument.
61
+ # This method does not exist within QueryProxy, it can only be called at the class level to create a new QP object.
62
+ # To set an identifier within a QueryProxy chain, give it as the first argument to a chained association.
63
+ #
64
+ # @example Start a new QueryProxy where the first identifier is set manually.
65
+ # # Generates: MATCH (s:`Student`), (result_lessons:`Lesson`), s-[rel1:`ENROLLED_IN`]->(result_lessons:`Lesson`)
66
+ # Student.as(:s).lessons
67
+ #
68
+ # @param [String, Symbol] node_var A string or symbol to use as the starting identifier.
69
+ # @return [Neo4j::ActiveNode::Query::QueryProxy]
70
+ def as(node_var)
71
+ query_proxy(node: node_var, context: self.name)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,65 @@
1
+ module Neo4j
2
+ module ActiveNode
3
+ module QueryMethods
4
+ def exists?(node_condition = nil)
5
+ unless node_condition.is_a?(Integer) || node_condition.is_a?(Hash) || node_condition.nil?
6
+ fail(Neo4j::InvalidParameterError, ':exists? only accepts ids or conditions')
7
+ end
8
+ query_start = exists_query_start(node_condition)
9
+ start_q = query_start.respond_to?(:query_as) ? query_start.query_as(:n) : query_start
10
+ start_q.return('COUNT(n) AS count').first.count > 0
11
+ end
12
+
13
+ # Returns the first node of this class, sorted by ID. Note that this may not be the first node created since Neo4j recycles IDs.
14
+ def first
15
+ self.query_as(:n).limit(1).order(n: primary_key).pluck(:n).first
16
+ end
17
+
18
+ # Returns the last node of this class, sorted by ID. Note that this may not be the first node created since Neo4j recycles IDs.
19
+ def last
20
+ self.query_as(:n).limit(1).order(n: {primary_key => :desc}).pluck(:n).first
21
+ end
22
+
23
+ # @return [Integer] number of nodes of this class
24
+ def count(distinct = nil)
25
+ fail(Neo4j::InvalidParameterError, ':count accepts `distinct` or nil as a parameter') unless distinct.nil? || distinct == :distinct
26
+ q = distinct.nil? ? 'n' : 'DISTINCT n'
27
+ self.query_as(:n).return("count(#{q}) AS count").first.count
28
+ end
29
+
30
+ alias_method :size, :count
31
+ alias_method :length, :count
32
+
33
+ def empty?
34
+ !self.all.exists?
35
+ end
36
+
37
+ alias_method :blank?, :empty?
38
+
39
+ def find_in_batches(options = {})
40
+ self.query_as(:n).return(:n).find_in_batches(:n, primary_key, options) do |batch|
41
+ yield batch.map(&:n)
42
+ end
43
+ end
44
+
45
+ def find_each(options = {})
46
+ self.query_as(:n).return(:n).find_each(:n, primary_key, options) do |batch|
47
+ yield batch.n
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def exists_query_start(node_condition)
54
+ case node_condition
55
+ when Integer
56
+ self.query_as(:n).where('ID(n)' => node_condition)
57
+ when Hash
58
+ self.where(node_condition.keys.first => node_condition.values.first)
59
+ else
60
+ self.query_as(:n)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end