activegraph 11.0.0.beta.1-java

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 (144) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +2016 -0
  3. data/CONTRIBUTORS +12 -0
  4. data/Gemfile +24 -0
  5. data/README.md +111 -0
  6. data/activegraph.gemspec +52 -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_graph.rb +123 -0
  12. data/lib/active_graph/ansi.rb +14 -0
  13. data/lib/active_graph/attribute_set.rb +32 -0
  14. data/lib/active_graph/base.rb +77 -0
  15. data/lib/active_graph/class_arguments.rb +39 -0
  16. data/lib/active_graph/config.rb +135 -0
  17. data/lib/active_graph/core.rb +14 -0
  18. data/lib/active_graph/core/connection_failed_error.rb +6 -0
  19. data/lib/active_graph/core/cypher_error.rb +37 -0
  20. data/lib/active_graph/core/entity.rb +11 -0
  21. data/lib/active_graph/core/instrumentable.rb +37 -0
  22. data/lib/active_graph/core/label.rb +135 -0
  23. data/lib/active_graph/core/logging.rb +44 -0
  24. data/lib/active_graph/core/node.rb +15 -0
  25. data/lib/active_graph/core/querable.rb +41 -0
  26. data/lib/active_graph/core/query.rb +485 -0
  27. data/lib/active_graph/core/query_builder.rb +18 -0
  28. data/lib/active_graph/core/query_clauses.rb +727 -0
  29. data/lib/active_graph/core/query_ext.rb +24 -0
  30. data/lib/active_graph/core/query_find_in_batches.rb +46 -0
  31. data/lib/active_graph/core/record.rb +51 -0
  32. data/lib/active_graph/core/result.rb +31 -0
  33. data/lib/active_graph/core/schema.rb +65 -0
  34. data/lib/active_graph/core/schema_errors.rb +12 -0
  35. data/lib/active_graph/core/wrappable.rb +30 -0
  36. data/lib/active_graph/errors.rb +59 -0
  37. data/lib/active_graph/lazy_attribute_hash.rb +38 -0
  38. data/lib/active_graph/migration.rb +148 -0
  39. data/lib/active_graph/migrations.rb +27 -0
  40. data/lib/active_graph/migrations/base.rb +77 -0
  41. data/lib/active_graph/migrations/check_pending.rb +20 -0
  42. data/lib/active_graph/migrations/helpers.rb +105 -0
  43. data/lib/active_graph/migrations/helpers/id_property.rb +72 -0
  44. data/lib/active_graph/migrations/helpers/relationships.rb +66 -0
  45. data/lib/active_graph/migrations/helpers/schema.rb +63 -0
  46. data/lib/active_graph/migrations/migration_file.rb +24 -0
  47. data/lib/active_graph/migrations/runner.rb +195 -0
  48. data/lib/active_graph/migrations/schema.rb +64 -0
  49. data/lib/active_graph/migrations/schema_migration.rb +14 -0
  50. data/lib/active_graph/model_schema.rb +139 -0
  51. data/lib/active_graph/node.rb +110 -0
  52. data/lib/active_graph/node/callbacks.rb +8 -0
  53. data/lib/active_graph/node/dependent.rb +11 -0
  54. data/lib/active_graph/node/dependent/association_methods.rb +49 -0
  55. data/lib/active_graph/node/dependent/query_proxy_methods.rb +52 -0
  56. data/lib/active_graph/node/dependent_callbacks.rb +31 -0
  57. data/lib/active_graph/node/enum.rb +26 -0
  58. data/lib/active_graph/node/has_n.rb +602 -0
  59. data/lib/active_graph/node/has_n/association.rb +278 -0
  60. data/lib/active_graph/node/has_n/association/rel_factory.rb +61 -0
  61. data/lib/active_graph/node/has_n/association/rel_wrapper.rb +23 -0
  62. data/lib/active_graph/node/has_n/association_cypher_methods.rb +108 -0
  63. data/lib/active_graph/node/id_property.rb +224 -0
  64. data/lib/active_graph/node/id_property/accessor.rb +62 -0
  65. data/lib/active_graph/node/initialize.rb +21 -0
  66. data/lib/active_graph/node/labels.rb +207 -0
  67. data/lib/active_graph/node/labels/index.rb +37 -0
  68. data/lib/active_graph/node/labels/reloading.rb +21 -0
  69. data/lib/active_graph/node/node_list_formatter.rb +13 -0
  70. data/lib/active_graph/node/node_wrapper.rb +54 -0
  71. data/lib/active_graph/node/orm_adapter.rb +82 -0
  72. data/lib/active_graph/node/persistence.rb +186 -0
  73. data/lib/active_graph/node/property.rb +60 -0
  74. data/lib/active_graph/node/query.rb +76 -0
  75. data/lib/active_graph/node/query/query_proxy.rb +367 -0
  76. data/lib/active_graph/node/query/query_proxy_eager_loading.rb +177 -0
  77. data/lib/active_graph/node/query/query_proxy_eager_loading/association_tree.rb +75 -0
  78. data/lib/active_graph/node/query/query_proxy_enumerable.rb +110 -0
  79. data/lib/active_graph/node/query/query_proxy_find_in_batches.rb +19 -0
  80. data/lib/active_graph/node/query/query_proxy_link.rb +139 -0
  81. data/lib/active_graph/node/query/query_proxy_methods.rb +303 -0
  82. data/lib/active_graph/node/query/query_proxy_methods_of_mass_updating.rb +99 -0
  83. data/lib/active_graph/node/query_methods.rb +68 -0
  84. data/lib/active_graph/node/reflection.rb +86 -0
  85. data/lib/active_graph/node/rels.rb +11 -0
  86. data/lib/active_graph/node/scope.rb +166 -0
  87. data/lib/active_graph/node/unpersisted.rb +48 -0
  88. data/lib/active_graph/node/validations.rb +59 -0
  89. data/lib/active_graph/paginated.rb +27 -0
  90. data/lib/active_graph/railtie.rb +108 -0
  91. data/lib/active_graph/relationship.rb +68 -0
  92. data/lib/active_graph/relationship/callbacks.rb +21 -0
  93. data/lib/active_graph/relationship/initialize.rb +28 -0
  94. data/lib/active_graph/relationship/persistence.rb +133 -0
  95. data/lib/active_graph/relationship/persistence/query_factory.rb +95 -0
  96. data/lib/active_graph/relationship/property.rb +92 -0
  97. data/lib/active_graph/relationship/query.rb +99 -0
  98. data/lib/active_graph/relationship/rel_wrapper.rb +31 -0
  99. data/lib/active_graph/relationship/related_node.rb +87 -0
  100. data/lib/active_graph/relationship/types.rb +80 -0
  101. data/lib/active_graph/relationship/validations.rb +8 -0
  102. data/lib/active_graph/schema/operation.rb +102 -0
  103. data/lib/active_graph/shared.rb +48 -0
  104. data/lib/active_graph/shared/attributes.rb +217 -0
  105. data/lib/active_graph/shared/callbacks.rb +66 -0
  106. data/lib/active_graph/shared/cypher.rb +37 -0
  107. data/lib/active_graph/shared/declared_properties.rb +204 -0
  108. data/lib/active_graph/shared/declared_property.rb +109 -0
  109. data/lib/active_graph/shared/declared_property/index.rb +37 -0
  110. data/lib/active_graph/shared/enum.rb +167 -0
  111. data/lib/active_graph/shared/filtered_hash.rb +79 -0
  112. data/lib/active_graph/shared/identity.rb +34 -0
  113. data/lib/active_graph/shared/initialize.rb +65 -0
  114. data/lib/active_graph/shared/marshal.rb +23 -0
  115. data/lib/active_graph/shared/mass_assignment.rb +63 -0
  116. data/lib/active_graph/shared/permitted_attributes.rb +28 -0
  117. data/lib/active_graph/shared/persistence.rb +272 -0
  118. data/lib/active_graph/shared/property.rb +249 -0
  119. data/lib/active_graph/shared/query_factory.rb +122 -0
  120. data/lib/active_graph/shared/rel_type_converters.rb +43 -0
  121. data/lib/active_graph/shared/serialized_properties.rb +30 -0
  122. data/lib/active_graph/shared/type_converters.rb +439 -0
  123. data/lib/active_graph/shared/typecasted_attributes.rb +99 -0
  124. data/lib/active_graph/shared/typecaster.rb +53 -0
  125. data/lib/active_graph/shared/validations.rb +44 -0
  126. data/lib/active_graph/tasks/migration.rake +204 -0
  127. data/lib/active_graph/timestamps.rb +11 -0
  128. data/lib/active_graph/timestamps/created.rb +9 -0
  129. data/lib/active_graph/timestamps/updated.rb +9 -0
  130. data/lib/active_graph/transaction.rb +22 -0
  131. data/lib/active_graph/transactions.rb +57 -0
  132. data/lib/active_graph/type_converters.rb +7 -0
  133. data/lib/active_graph/undeclared_properties.rb +53 -0
  134. data/lib/active_graph/version.rb +3 -0
  135. data/lib/active_graph/wrapper.rb +4 -0
  136. data/lib/rails/generators/active_graph/migration/migration_generator.rb +16 -0
  137. data/lib/rails/generators/active_graph/migration/templates/migration.erb +9 -0
  138. data/lib/rails/generators/active_graph/model/model_generator.rb +89 -0
  139. data/lib/rails/generators/active_graph/model/templates/migration.erb +11 -0
  140. data/lib/rails/generators/active_graph/model/templates/model.erb +15 -0
  141. data/lib/rails/generators/active_graph/upgrade_v8/templates/migration.erb +17 -0
  142. data/lib/rails/generators/active_graph/upgrade_v8/upgrade_v8_generator.rb +34 -0
  143. data/lib/rails/generators/active_graph_generator.rb +121 -0
  144. metadata +423 -0
@@ -0,0 +1,76 @@
1
+ module ActiveGraph
2
+ module Node
3
+ # Helper methods to return ActiveGraph::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 [ActiveGraph::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 [ActiveGraph::Node::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 [ActiveGraph::Core::Query]
46
+ def query_as(var, with_labels = true)
47
+ query_proxy.query_as(var, with_labels)
48
+ end
49
+
50
+ ActiveGraph::Node::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
+ ActiveGraph::Node::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 [ActiveGraph::Node::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,367 @@
1
+ module ActiveGraph
2
+ module Node
3
+ module Query
4
+ # rubocop:disable Metrics/ClassLength
5
+ class QueryProxy
6
+ # rubocop:enable Metrics/ClassLength
7
+ include ActiveGraph::Node::Query::QueryProxyEnumerable
8
+ include ActiveGraph::Node::Query::QueryProxyMethods
9
+ include ActiveGraph::Node::Query::QueryProxyMethodsOfMassUpdating
10
+ include ActiveGraph::Node::Query::QueryProxyFindInBatches
11
+ include ActiveGraph::Node::Query::QueryProxyEagerLoading
12
+ include ActiveGraph::Node::Dependent::QueryProxyMethods
13
+
14
+ # The most recent node to start a QueryProxy chain.
15
+ # Will be nil when using QueryProxy chains on class methods.
16
+ attr_reader :source_object, :association, :model, :starting_query
17
+
18
+ # QueryProxy is Node's Cypher DSL. While the name might imply that it creates queries in a general sense,
19
+ # it is actually referring to <tt>ActiveGraph::Core::Query</tt>, which is a pure Ruby Cypher DSL provided by the <tt>activegraph</tt> gem.
20
+ # QueryProxy provides ActiveRecord-like methods for common patterns. When it's not handling CRUD for relationships and queries, it
21
+ # provides Node's association chaining (`student.lessons.teachers.where(age: 30).hobbies`) and enjoys long walks on the
22
+ # beach.
23
+ #
24
+ # It should not ever be necessary to instantiate a new QueryProxy object directly, it always happens as a result of
25
+ # calling a method that makes use of it.
26
+ #
27
+ # @param [Constant] model The class which included Node (typically a model, hence the name) from which the query
28
+ # originated.
29
+ # @param [ActiveGraph::Node::HasN::Association] association The Node association (an object created by a <tt>has_one</tt> or
30
+ # <tt>has_many</tt>) that created this object.
31
+ # @param [Hash] options Additional options pertaining to the QueryProxy object. These may include:
32
+ # @option options [String, Symbol] :node_var A string or symbol to be used by Cypher within its query string as an identifier
33
+ # @option options [String, Symbol] :rel_var Same as above but pertaining to a relationship identifier
34
+ # @option options [Range, Integer, Symbol, Hash] :rel_length A Range, a Integer, a Hash or a Symbol to indicate the variable-length/fixed-length
35
+ # qualifier of the relationship. See http://neo4jrb.readthedocs.org/en/latest/Querying.html#variable-length-relationships.
36
+ # @option options [ActiveGraph::Node] :source_object The node instance at the start of the QueryProxy chain
37
+ # @option options [QueryProxy] :query_proxy An existing QueryProxy chain upon which this new object should be built
38
+ #
39
+ # QueryProxy objects are evaluated lazily.
40
+ def initialize(model, association = nil, options = {})
41
+ @model = model
42
+ @association = association
43
+ @context = options.delete(:context)
44
+ @options = options
45
+ @associations_spec = []
46
+
47
+ instance_vars_from_options!(options)
48
+
49
+ @match_type = @optional ? :optional_match : :match
50
+
51
+ @rel_var = options[:rel] || _rel_chain_var
52
+
53
+ @chain = []
54
+ @params = @query_proxy ? @query_proxy.instance_variable_get('@params') : {}
55
+ end
56
+
57
+ def inspect
58
+ formatted_nodes = ActiveGraph::Node::NodeListFormatter.new(to_a)
59
+ "#<QueryProxy #{@context} #{formatted_nodes.inspect}>"
60
+ end
61
+
62
+ attr_reader :start_object, :query_proxy
63
+
64
+ # The current node identifier on deck, so to speak. It is the object that will be returned by calling `each` and the last node link
65
+ # in the QueryProxy chain.
66
+ attr_reader :node_var
67
+ def identity
68
+ @node_var || _result_string(_chain_level + 1)
69
+ end
70
+ alias node_identity identity
71
+
72
+ # The relationship identifier most recently used by the QueryProxy chain.
73
+ attr_reader :rel_var
74
+ def rel_identity
75
+ ActiveSupport::Deprecation.warn 'rel_identity is deprecated and may be removed from future releases, use rel_var instead.', caller
76
+
77
+ @rel_var
78
+ end
79
+
80
+ def params(params)
81
+ new_link.tap { |new_query| new_query._add_params(params) }
82
+ end
83
+
84
+ # Like calling #query_as, but for when you don't care about the variable name
85
+ def query
86
+ query_as(identity)
87
+ end
88
+
89
+ # Build a ActiveGraph::Core::Query object for the QueryProxy. This is necessary when you want to take an existing QueryProxy chain
90
+ # and work with it from the more powerful (but less friendly) ActiveGraph::Core::Query.
91
+ # @param [String,Symbol] var The identifier to use for node at this link of the QueryProxy chain.
92
+ #
93
+ # .. code-block:: ruby
94
+ #
95
+ # student.lessons.query_as(:l).with('your cypher here...')
96
+ def query_as(var, with_labels = true)
97
+ query_from_chain(chain, base_query(var, with_labels).params(@params), var)
98
+ .tap { |query| query.proxy_chain_level = _chain_level }
99
+ end
100
+
101
+ def query_from_chain(chain, base_query, var)
102
+ chain.inject(base_query) do |query, link|
103
+ args = link.args(var, rel_var)
104
+
105
+ args.is_a?(Array) ? query.send(link.clause, *args) : query.send(link.clause, args)
106
+ end
107
+ end
108
+
109
+ def base_query(var, with_labels = true)
110
+ if @association
111
+ chain_var = _association_chain_var
112
+ (_association_query_start(chain_var) & _query).break.send(@match_type,
113
+ "(#{chain_var})#{_association_arrow}(#{var}#{_model_label_string})")
114
+ else
115
+ starting_query ? starting_query : _query_model_as(var, with_labels)
116
+ end
117
+ end
118
+
119
+ # param [TrueClass, FalseClass] with_labels This param is used by certain QueryProxy methods that already have the neo_id and
120
+ # therefore do not need labels.
121
+ # The @association_labels instance var is set during init and used during association chaining to keep labels out of Cypher queries.
122
+ def _model_label_string(with_labels = true)
123
+ return if !@model || (!with_labels || @association_labels == false)
124
+ @model.mapped_label_names.map { |label_name| ":`#{label_name}`" }.join
125
+ end
126
+
127
+ # Scope all queries to the current scope.
128
+ #
129
+ # .. code-block:: ruby
130
+ #
131
+ # Comment.where(post_id: 1).scoping do
132
+ # Comment.first
133
+ # end
134
+ #
135
+ # TODO: unscoped
136
+ # Please check unscoped if you want to remove all previous scopes (including
137
+ # the default_scope) during the execution of a block.
138
+ def scoping
139
+ previous = @model.current_scope
140
+ @model.current_scope = self
141
+ yield
142
+ ensure
143
+ @model.current_scope = previous
144
+ end
145
+
146
+ METHODS = %w(where where_not rel_where rel_where_not rel_order order skip limit)
147
+
148
+ METHODS.each do |method|
149
+ define_method(method) { |*args| build_deeper_query_proxy(method.to_sym, args) }
150
+ end
151
+ # Since there are rel_where and rel_order methods, it seems only natural for there to be node_where and node_order
152
+ alias node_where where
153
+ alias node_order order
154
+ alias offset skip
155
+ alias order_by order
156
+
157
+ # Cypher string for the QueryProxy's query. This will not include params. For the full output, see <tt>to_cypher_with_params</tt>.
158
+ delegate :to_cypher, to: :query
159
+
160
+ delegate :print_cypher, to: :query
161
+
162
+ # Returns a string of the cypher query with return objects and params
163
+ # @param [Array] columns array containing symbols of identifiers used in the query
164
+ # @return [String]
165
+ def to_cypher_with_params(columns = [self.identity])
166
+ final_query = query.return_query(columns)
167
+ "#{final_query.to_cypher} | params: #{final_query.send(:merge_params)}"
168
+ end
169
+
170
+ # To add a relationship for the node for the association on this QueryProxy
171
+ def <<(other_node)
172
+ _create_relation_or_defer(other_node)
173
+ self
174
+ end
175
+
176
+ # Executes the relation chain specified in the block, while keeping the current scope
177
+ #
178
+ # @example Load all people that have friends
179
+ # Person.all.branch { friends }.to_a # => Returns a list of `Person`
180
+ #
181
+ # @example Load all people that has old friends
182
+ # Person.all.branch { friends.where('age > 70') }.to_a # => Returns a list of `Person`
183
+ #
184
+ # @yield the block that will be evaluated starting from the current scope
185
+ #
186
+ # @return [QueryProxy] A new QueryProxy
187
+ def branch(&block)
188
+ fail LocalJumpError, 'no block given' if block.nil?
189
+ # `as(identity)` is here to make sure we get the right variable
190
+ # There might be a deeper problem of the variable changing when we
191
+ # traverse an association
192
+ as(identity).instance_eval(&block).query.proxy_as(self.model, identity).tap do |new_query_proxy|
193
+ propagate_context(new_query_proxy)
194
+ end
195
+ end
196
+
197
+ def [](index)
198
+ # TODO: Maybe for this and other methods, use array if already loaded, otherwise
199
+ # use OFFSET and LIMIT 1?
200
+ self.to_a[index]
201
+ end
202
+
203
+ def create(other_nodes, properties = {})
204
+ fail 'Can only create relationships on associations' if !@association
205
+ other_nodes = _nodeify!(*other_nodes)
206
+
207
+ ActiveGraph::Base.transaction do
208
+ other_nodes.each do |other_node|
209
+ if other_node.neo_id
210
+ other_node.try(:delete_reverse_has_one_core_rel, association)
211
+ else
212
+ other_node.save
213
+ end
214
+
215
+ @start_object.association_proxy_cache.clear
216
+
217
+ _create_relationship(other_node, properties)
218
+ end
219
+ end
220
+ end
221
+
222
+ def _nodeify!(*args)
223
+ other_nodes = [args].flatten!.map! do |arg|
224
+ (arg.is_a?(Integer) || arg.is_a?(String)) ? @model.find_by(id: arg) : arg
225
+ end.compact
226
+
227
+ if @model && other_nodes.any? { |other_node| !other_node.class.mapped_label_names.include?(@model.mapped_label_name) }
228
+ fail ArgumentError, "Node must be of the association's class when model is specified"
229
+ end
230
+
231
+ other_nodes
232
+ end
233
+
234
+ def _create_relationship(other_node_or_nodes, properties)
235
+ association._create_relationship(@start_object, other_node_or_nodes, properties)
236
+ end
237
+
238
+ def read_attribute_for_serialization(*args)
239
+ to_a.map { |o| o.read_attribute_for_serialization(*args) }
240
+ end
241
+
242
+ delegate :to_ary, to: :to_a
243
+
244
+ # QueryProxy objects act as a representation of a model at the class level so we pass through calls
245
+ # This allows us to define class functions for reusable query chaining or for end-of-query aggregation/summarizing
246
+ def method_missing(method_name, *args, &block)
247
+ if @model && @model.respond_to?(method_name)
248
+ scoping { @model.public_send(method_name, *args, &block) }
249
+ else
250
+ super
251
+ end
252
+ end
253
+
254
+ def respond_to_missing?(method_name, include_all = false)
255
+ (@model && @model.respond_to?(method_name, include_all)) || super
256
+ end
257
+
258
+ def optional?
259
+ @optional == true
260
+ end
261
+
262
+ attr_reader :context
263
+
264
+ def new_link(node_var = nil)
265
+ self.clone.tap do |new_query_proxy|
266
+ new_query_proxy.instance_variable_set('@result_cache', nil)
267
+ new_query_proxy.instance_variable_set('@node_var', node_var) if node_var
268
+ end
269
+ end
270
+
271
+ def unpersisted_start_object?
272
+ @start_object && @start_object.new_record?
273
+ end
274
+
275
+ protected
276
+
277
+ def _create_relation_or_defer(other_node)
278
+ if @start_object._persisted_obj
279
+ create(other_node, {})
280
+ elsif @association
281
+ @start_object.defer_create(@association.name, other_node)
282
+ else
283
+ fail 'Another crazy error!'
284
+ end
285
+ end
286
+
287
+ # Methods are underscored to prevent conflict with user class methods
288
+ def _add_params(params)
289
+ @params = @params.merge(params)
290
+ end
291
+
292
+ def _add_links(links)
293
+ @chain += links
294
+ end
295
+
296
+ def _query_model_as(var, with_labels = true)
297
+ _query.break.send(@match_type, _match_arg(var, with_labels))
298
+ end
299
+
300
+ # @param [String, Symbol] var The Cypher identifier to use within the match string
301
+ # @param [Boolean] with_labels Send "true" to include model labels where possible.
302
+ def _match_arg(var, with_labels)
303
+ if @model && with_labels != false
304
+ labels = @model.respond_to?(:mapped_label_names) ? _model_label_string : @model
305
+ {var.to_sym => labels}
306
+ else
307
+ var.to_sym
308
+ end
309
+ end
310
+
311
+ def _query
312
+ ActiveGraph::Base.new_query(context: @context)
313
+ end
314
+
315
+ def _result_string(index = nil)
316
+ "result_#{(association || model).try(:name)}#{index}".downcase.tr(':', '').to_sym
317
+ end
318
+
319
+ def _association_arrow(properties = {}, create = false)
320
+ @association && @association.arrow_cypher(@rel_var, properties, create, false, @rel_length)
321
+ end
322
+
323
+ def _chain_level
324
+ (@query_proxy ? @query_proxy._chain_level : (@chain_level || 0)) + 1
325
+ end
326
+
327
+ def _association_chain_var
328
+ fail 'Crazy error' if !(start_object || @query_proxy)
329
+
330
+ if start_object
331
+ :"#{start_object.class.name.gsub('::', '_').downcase}#{start_object.neo_id}"
332
+ else
333
+ @query_proxy.node_var || :"node#{_chain_level}"
334
+ end
335
+ end
336
+
337
+ def _association_query_start(var)
338
+ # TODO: Better error
339
+ fail 'Crazy error' if !(object = (start_object || @query_proxy))
340
+
341
+ object.query_as(var)
342
+ end
343
+
344
+ def _rel_chain_var
345
+ :"rel#{_chain_level - 1}"
346
+ end
347
+
348
+ attr_writer :context
349
+
350
+ private
351
+
352
+ def instance_vars_from_options!(options)
353
+ @node_var, @source_object, @starting_query, @optional,
354
+ @start_object, @query_proxy, @chain_level, @association_labels,
355
+ @rel_length = options.values_at(:node, :source_object, :starting_query, :optional,
356
+ :start_object, :query_proxy, :chain_level, :association_labels, :rel_length)
357
+ end
358
+
359
+ def build_deeper_query_proxy(method, args)
360
+ new_link.tap do |new_query_proxy|
361
+ Link.for_args(@model, method, args, association).each { |link| new_query_proxy._add_links(link) }
362
+ end
363
+ end
364
+ end
365
+ end
366
+ end
367
+ end
@@ -0,0 +1,177 @@
1
+ module ActiveGraph
2
+ module Node
3
+ module Query
4
+ module QueryProxyEagerLoading
5
+ class IdentityMap < Hash
6
+ def add(node)
7
+ self[node.neo_id] ||= node
8
+ end
9
+ end
10
+
11
+ def pluck_vars(node, rel)
12
+ with_associations_tree.empty? ? super : perform_query
13
+ end
14
+
15
+ def perform_query
16
+ @_cache = IdentityMap.new
17
+ build_query
18
+ .map do |record, eager_data|
19
+ record = cache_and_init(record, with_associations_tree)
20
+ eager_data.zip(with_associations_tree.paths.map(&:last)).each do |eager_records, element|
21
+ eager_records.first.zip(eager_records.last).each do |eager_record|
22
+ add_to_cache(*eager_record, element)
23
+ end
24
+ end
25
+ record
26
+ end
27
+ end
28
+
29
+ def with_associations(*spec)
30
+ new_link.tap do |new_query_proxy|
31
+ new_query_proxy.with_associations_tree = with_associations_tree.clone
32
+ new_query_proxy.with_associations_tree.add_spec(spec)
33
+ end
34
+ end
35
+
36
+ def propagate_context(query_proxy)
37
+ super
38
+ query_proxy.instance_variable_set('@with_associations_tree', @with_associations_tree)
39
+ end
40
+
41
+ def with_associations_tree
42
+ @with_associations_tree ||= association_tree_class.new(model)
43
+ end
44
+
45
+ def association_tree_class
46
+ AssociationTree
47
+ end
48
+
49
+ def with_associations_tree=(tree)
50
+ @with_associations_tree = tree
51
+ end
52
+
53
+ def first
54
+ (query.clause?(:order) ? self : order(order_property)).limit(1).to_a.first
55
+ end
56
+
57
+ private
58
+
59
+ def add_to_cache(rel, node, element)
60
+ direction = element.association.direction
61
+ node = cache_and_init(node, element)
62
+ if rel.is_a?(ActiveGraph::Relationship)
63
+ rel.instance_variable_set(direction == :in ? '@from_node' : '@to_node', node)
64
+ end
65
+ @_cache[direction == :out ? rel.start_node_id : rel.end_node_id]
66
+ .association_proxy(element.name).add_to_cache(node, rel)
67
+ end
68
+
69
+ def init_associations(node, element)
70
+ element.each_key { |key| node.association_proxy(key).init_cache }
71
+ node.association_proxy(element.name).init_cache if element.rel_length == ''
72
+ end
73
+
74
+ def cache_and_init(node, element)
75
+ @_cache.add(node).tap { |n| init_associations(n, element) }
76
+ end
77
+
78
+ def with_associations_return_clause
79
+ path_names.map { |n| var(n, :collection, &:itself) }.join(',')
80
+ end
81
+
82
+ def var(*parts)
83
+ yield(escape(parts.compact.join('_')))
84
+ end
85
+
86
+ # In neo4j version 2.1.8 this fails due to a bug:
87
+ # MATCH (`n`) WITH `n` RETURN `n`
88
+ # but this
89
+ # MATCH (`n`) WITH n RETURN `n`
90
+ # and this
91
+ # MATCH (`n`) WITH `n` AS `n` RETURN `n`
92
+ # does not
93
+ def var_fix(*var)
94
+ var(*var, &method(:as_alias))
95
+ end
96
+
97
+ def as_alias(var)
98
+ "#{var} AS #{var}"
99
+ end
100
+
101
+ def escape(s)
102
+ "`#{s}`"
103
+ end
104
+
105
+ def path_name(path)
106
+ path.map(&:name).join('.')
107
+ end
108
+
109
+ def path_names
110
+ with_associations_tree.paths.map { |path| path_name(path) }
111
+ end
112
+
113
+ def build_query
114
+ before_pluck(query_from_association_tree).pluck(identity, "[#{with_associations_return_clause}]")
115
+ end
116
+
117
+ def before_pluck(query)
118
+ query_from_chain(@order_chain, query, identity)
119
+ end
120
+
121
+ def query_from_association_tree
122
+ previous_with_vars = []
123
+ with_associations_tree.paths.inject(query_as(identity).with(ensure_distinct(identity))) do |query, path|
124
+ with_association_query_part(query, path, previous_with_vars).tap do
125
+ previous_with_vars << var_fix(path_name(path), :collection)
126
+ end
127
+ end
128
+ end
129
+
130
+ def with_association_query_part(base_query, path, previous_with_vars)
131
+ optional_match_with_where(base_query, path, previous_with_vars)
132
+ .with(identity,
133
+ "[#{relationship_collection(path)}, collect(#{escape path_name(path)})] "\
134
+ "AS #{escape("#{path_name(path)}_collection")}",
135
+ *previous_with_vars)
136
+ end
137
+
138
+ def relationship_collection(path)
139
+ path.last.rel_length ? "collect(last(relationships(#{escape("#{path_name(path)}_path")})))" : "collect(#{escape("#{path_name(path)}_rel")})"
140
+ end
141
+
142
+ def optional_match_with_where(base_query, path, _)
143
+ path
144
+ .each_with_index.map { |_, index| path[0..index] }
145
+ .inject(optional_match(base_query, path)) do |query, path_prefix|
146
+ query.where(path_prefix.last.association.target_where_clause(escape(path_name(path_prefix))))
147
+ end
148
+ end
149
+
150
+ def optional_match(base_query, path)
151
+ start_path = "#{escape("#{path_name(path)}_path")}=(#{identity})"
152
+ base_query.optional_match(
153
+ "#{start_path}#{path.each_with_index.map do |element, index|
154
+ relationship_part(element.association, path_name(path[0..index]), element.rel_length)
155
+ end.join}"
156
+ )
157
+ end
158
+
159
+ def relationship_part(association, path_name, rel_length)
160
+ if rel_length
161
+ rel_name = nil
162
+ length = {max: rel_length}
163
+ else
164
+ rel_name = escape("#{path_name}_rel")
165
+ length = nil
166
+ end
167
+ "#{association.arrow_cypher(rel_name, {}, false, false, length)}(#{escape(path_name)})"
168
+ end
169
+
170
+ def chain
171
+ @order_chain = @chain.select { |link| link.clause == :order } unless with_associations_tree.empty?
172
+ @chain
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end