activegraph 11.0.0.beta.1-java
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +2016 -0
- data/CONTRIBUTORS +12 -0
- data/Gemfile +24 -0
- data/README.md +111 -0
- data/activegraph.gemspec +52 -0
- data/bin/rake +17 -0
- data/config/locales/en.yml +5 -0
- data/config/neo4j/add_classnames.yml +1 -0
- data/config/neo4j/config.yml +35 -0
- data/lib/active_graph.rb +123 -0
- data/lib/active_graph/ansi.rb +14 -0
- data/lib/active_graph/attribute_set.rb +32 -0
- data/lib/active_graph/base.rb +77 -0
- data/lib/active_graph/class_arguments.rb +39 -0
- data/lib/active_graph/config.rb +135 -0
- data/lib/active_graph/core.rb +14 -0
- data/lib/active_graph/core/connection_failed_error.rb +6 -0
- data/lib/active_graph/core/cypher_error.rb +37 -0
- data/lib/active_graph/core/entity.rb +11 -0
- data/lib/active_graph/core/instrumentable.rb +37 -0
- data/lib/active_graph/core/label.rb +135 -0
- data/lib/active_graph/core/logging.rb +44 -0
- data/lib/active_graph/core/node.rb +15 -0
- data/lib/active_graph/core/querable.rb +41 -0
- data/lib/active_graph/core/query.rb +485 -0
- data/lib/active_graph/core/query_builder.rb +18 -0
- data/lib/active_graph/core/query_clauses.rb +727 -0
- data/lib/active_graph/core/query_ext.rb +24 -0
- data/lib/active_graph/core/query_find_in_batches.rb +46 -0
- data/lib/active_graph/core/record.rb +51 -0
- data/lib/active_graph/core/result.rb +31 -0
- data/lib/active_graph/core/schema.rb +65 -0
- data/lib/active_graph/core/schema_errors.rb +12 -0
- data/lib/active_graph/core/wrappable.rb +30 -0
- data/lib/active_graph/errors.rb +59 -0
- data/lib/active_graph/lazy_attribute_hash.rb +38 -0
- data/lib/active_graph/migration.rb +148 -0
- data/lib/active_graph/migrations.rb +27 -0
- data/lib/active_graph/migrations/base.rb +77 -0
- data/lib/active_graph/migrations/check_pending.rb +20 -0
- data/lib/active_graph/migrations/helpers.rb +105 -0
- data/lib/active_graph/migrations/helpers/id_property.rb +72 -0
- data/lib/active_graph/migrations/helpers/relationships.rb +66 -0
- data/lib/active_graph/migrations/helpers/schema.rb +63 -0
- data/lib/active_graph/migrations/migration_file.rb +24 -0
- data/lib/active_graph/migrations/runner.rb +195 -0
- data/lib/active_graph/migrations/schema.rb +64 -0
- data/lib/active_graph/migrations/schema_migration.rb +14 -0
- data/lib/active_graph/model_schema.rb +139 -0
- data/lib/active_graph/node.rb +110 -0
- data/lib/active_graph/node/callbacks.rb +8 -0
- data/lib/active_graph/node/dependent.rb +11 -0
- data/lib/active_graph/node/dependent/association_methods.rb +49 -0
- data/lib/active_graph/node/dependent/query_proxy_methods.rb +52 -0
- data/lib/active_graph/node/dependent_callbacks.rb +31 -0
- data/lib/active_graph/node/enum.rb +26 -0
- data/lib/active_graph/node/has_n.rb +602 -0
- data/lib/active_graph/node/has_n/association.rb +278 -0
- data/lib/active_graph/node/has_n/association/rel_factory.rb +61 -0
- data/lib/active_graph/node/has_n/association/rel_wrapper.rb +23 -0
- data/lib/active_graph/node/has_n/association_cypher_methods.rb +108 -0
- data/lib/active_graph/node/id_property.rb +224 -0
- data/lib/active_graph/node/id_property/accessor.rb +62 -0
- data/lib/active_graph/node/initialize.rb +21 -0
- data/lib/active_graph/node/labels.rb +207 -0
- data/lib/active_graph/node/labels/index.rb +37 -0
- data/lib/active_graph/node/labels/reloading.rb +21 -0
- data/lib/active_graph/node/node_list_formatter.rb +13 -0
- data/lib/active_graph/node/node_wrapper.rb +54 -0
- data/lib/active_graph/node/orm_adapter.rb +82 -0
- data/lib/active_graph/node/persistence.rb +186 -0
- data/lib/active_graph/node/property.rb +60 -0
- data/lib/active_graph/node/query.rb +76 -0
- data/lib/active_graph/node/query/query_proxy.rb +367 -0
- data/lib/active_graph/node/query/query_proxy_eager_loading.rb +177 -0
- data/lib/active_graph/node/query/query_proxy_eager_loading/association_tree.rb +75 -0
- data/lib/active_graph/node/query/query_proxy_enumerable.rb +110 -0
- data/lib/active_graph/node/query/query_proxy_find_in_batches.rb +19 -0
- data/lib/active_graph/node/query/query_proxy_link.rb +139 -0
- data/lib/active_graph/node/query/query_proxy_methods.rb +303 -0
- data/lib/active_graph/node/query/query_proxy_methods_of_mass_updating.rb +99 -0
- data/lib/active_graph/node/query_methods.rb +68 -0
- data/lib/active_graph/node/reflection.rb +86 -0
- data/lib/active_graph/node/rels.rb +11 -0
- data/lib/active_graph/node/scope.rb +166 -0
- data/lib/active_graph/node/unpersisted.rb +48 -0
- data/lib/active_graph/node/validations.rb +59 -0
- data/lib/active_graph/paginated.rb +27 -0
- data/lib/active_graph/railtie.rb +108 -0
- data/lib/active_graph/relationship.rb +68 -0
- data/lib/active_graph/relationship/callbacks.rb +21 -0
- data/lib/active_graph/relationship/initialize.rb +28 -0
- data/lib/active_graph/relationship/persistence.rb +133 -0
- data/lib/active_graph/relationship/persistence/query_factory.rb +95 -0
- data/lib/active_graph/relationship/property.rb +92 -0
- data/lib/active_graph/relationship/query.rb +99 -0
- data/lib/active_graph/relationship/rel_wrapper.rb +31 -0
- data/lib/active_graph/relationship/related_node.rb +87 -0
- data/lib/active_graph/relationship/types.rb +80 -0
- data/lib/active_graph/relationship/validations.rb +8 -0
- data/lib/active_graph/schema/operation.rb +102 -0
- data/lib/active_graph/shared.rb +48 -0
- data/lib/active_graph/shared/attributes.rb +217 -0
- data/lib/active_graph/shared/callbacks.rb +66 -0
- data/lib/active_graph/shared/cypher.rb +37 -0
- data/lib/active_graph/shared/declared_properties.rb +204 -0
- data/lib/active_graph/shared/declared_property.rb +109 -0
- data/lib/active_graph/shared/declared_property/index.rb +37 -0
- data/lib/active_graph/shared/enum.rb +167 -0
- data/lib/active_graph/shared/filtered_hash.rb +79 -0
- data/lib/active_graph/shared/identity.rb +34 -0
- data/lib/active_graph/shared/initialize.rb +65 -0
- data/lib/active_graph/shared/marshal.rb +23 -0
- data/lib/active_graph/shared/mass_assignment.rb +63 -0
- data/lib/active_graph/shared/permitted_attributes.rb +28 -0
- data/lib/active_graph/shared/persistence.rb +272 -0
- data/lib/active_graph/shared/property.rb +249 -0
- data/lib/active_graph/shared/query_factory.rb +122 -0
- data/lib/active_graph/shared/rel_type_converters.rb +43 -0
- data/lib/active_graph/shared/serialized_properties.rb +30 -0
- data/lib/active_graph/shared/type_converters.rb +439 -0
- data/lib/active_graph/shared/typecasted_attributes.rb +99 -0
- data/lib/active_graph/shared/typecaster.rb +53 -0
- data/lib/active_graph/shared/validations.rb +44 -0
- data/lib/active_graph/tasks/migration.rake +204 -0
- data/lib/active_graph/timestamps.rb +11 -0
- data/lib/active_graph/timestamps/created.rb +9 -0
- data/lib/active_graph/timestamps/updated.rb +9 -0
- data/lib/active_graph/transaction.rb +22 -0
- data/lib/active_graph/transactions.rb +57 -0
- data/lib/active_graph/type_converters.rb +7 -0
- data/lib/active_graph/undeclared_properties.rb +53 -0
- data/lib/active_graph/version.rb +3 -0
- data/lib/active_graph/wrapper.rb +4 -0
- data/lib/rails/generators/active_graph/migration/migration_generator.rb +16 -0
- data/lib/rails/generators/active_graph/migration/templates/migration.erb +9 -0
- data/lib/rails/generators/active_graph/model/model_generator.rb +89 -0
- data/lib/rails/generators/active_graph/model/templates/migration.erb +11 -0
- data/lib/rails/generators/active_graph/model/templates/model.erb +15 -0
- data/lib/rails/generators/active_graph/upgrade_v8/templates/migration.erb +17 -0
- data/lib/rails/generators/active_graph/upgrade_v8/upgrade_v8_generator.rb +34 -0
- data/lib/rails/generators/active_graph_generator.rb +121 -0
- 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
|