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