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,75 @@
|
|
|
1
|
+
module ActiveGraph
|
|
2
|
+
module Node
|
|
3
|
+
module Query
|
|
4
|
+
module QueryProxyEagerLoading
|
|
5
|
+
class AssociationTree < Hash
|
|
6
|
+
attr_accessor :model, :name, :association, :path, :rel_length
|
|
7
|
+
|
|
8
|
+
def initialize(model, name = nil, rel_length = nil)
|
|
9
|
+
super()
|
|
10
|
+
self.model = name ? target_class(model, name) : model
|
|
11
|
+
self.name = name
|
|
12
|
+
self.association = name ? model.associations[name] : nil
|
|
13
|
+
self.rel_length = rel_length
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def clone
|
|
17
|
+
super.tap { |copy| copy.each { |key, value| copy[key] = value.clone } }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add_spec(spec)
|
|
21
|
+
fail_spec(spec) unless model
|
|
22
|
+
|
|
23
|
+
case spec
|
|
24
|
+
when nil
|
|
25
|
+
nil
|
|
26
|
+
when Array
|
|
27
|
+
spec.each(&method(:add_spec))
|
|
28
|
+
when Hash
|
|
29
|
+
process_hash(spec)
|
|
30
|
+
when String
|
|
31
|
+
process_string(spec)
|
|
32
|
+
else
|
|
33
|
+
add_key(spec)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def fail_spec(spec)
|
|
38
|
+
fail "Cannot eager load \"past\" a polymorphic association. \
|
|
39
|
+
(Since the association can return multiple models, we don't how to handle the \"#{spec}\" association.)"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def paths(*prefix)
|
|
43
|
+
values.flat_map { |v| [[*prefix, v]] + v.paths(*prefix, v) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def process_hash(spec)
|
|
47
|
+
spec.each { |key, value| add_nested(key, value) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def add_key(key, length = nil)
|
|
51
|
+
self[key] ||= self.class.new(model, key, length)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def add_nested(key, value, length = nil)
|
|
55
|
+
add_key(key, length).add_spec(value)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def process_string(str)
|
|
59
|
+
head, rest = str.split('.', 2)
|
|
60
|
+
k, length = head.split('*', -2)
|
|
61
|
+
add_nested(k.to_sym, rest, length)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def target_class(model, key)
|
|
67
|
+
association = model.associations[key]
|
|
68
|
+
fail "Invalid association: #{[*path, key].join('.')}" unless association
|
|
69
|
+
model.associations[key].target_class
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
module ActiveGraph
|
|
2
|
+
module Node
|
|
3
|
+
module Query
|
|
4
|
+
# Methods related to returning nodes and rels from QueryProxy
|
|
5
|
+
module QueryProxyEnumerable
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
# Just like every other <tt>each</tt> but it allows for optional params to support the versions that also return relationships.
|
|
9
|
+
# The <tt>node</tt> and <tt>rel</tt> params are typically used by those other methods but there's nothing stopping you from
|
|
10
|
+
# using `your_node.each(true, true)` instead of `your_node.each_with_rel`.
|
|
11
|
+
# @return [Enumerable] An enumerable containing some combination of nodes and rels.
|
|
12
|
+
def each(node = true, rel = nil, &block)
|
|
13
|
+
result(node, rel).each(&block)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def result(node = true, rel = nil)
|
|
17
|
+
return [].freeze if unpersisted_start_object?
|
|
18
|
+
|
|
19
|
+
@result_cache ||= {}
|
|
20
|
+
return result_cache_for(node, rel) if result_cache?(node, rel)
|
|
21
|
+
|
|
22
|
+
result = pluck_vars(node, rel)
|
|
23
|
+
set_instance_caches(result, node, rel)
|
|
24
|
+
|
|
25
|
+
@result_cache[[node, rel]] ||= result
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def result_cache?(node = true, rel = nil)
|
|
29
|
+
!!result_cache_for(node, rel)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def result_cache_for(node = true, rel = nil)
|
|
33
|
+
(@result_cache || {})[[node, rel]]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fetch_result_cache
|
|
37
|
+
@result_cache ||= yield
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# When called at the end of a QueryProxy chain, it will return the resultant relationship objects intead of nodes.
|
|
41
|
+
# For example, to return the relationship between a given student and their lessons:
|
|
42
|
+
#
|
|
43
|
+
# .. code-block:: ruby
|
|
44
|
+
#
|
|
45
|
+
# student.lessons.each_rel do |rel|
|
|
46
|
+
#
|
|
47
|
+
# @return [Enumerable] An enumerable containing any number of applicable relationship objects.
|
|
48
|
+
def each_rel(&block)
|
|
49
|
+
block_given? ? each(false, true, &block) : to_enum(:each, false, true)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# When called at the end of a QueryProxy chain, it will return the nodes and relationships of the last link.
|
|
53
|
+
# For example, to return a lesson and each relationship to a given student:
|
|
54
|
+
#
|
|
55
|
+
# .. code-block:: ruby
|
|
56
|
+
#
|
|
57
|
+
# student.lessons.each_with_rel do |lesson, rel|
|
|
58
|
+
def each_with_rel(&block)
|
|
59
|
+
block_given? ? each(true, true, &block) : to_enum(:each, true, true)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Does exactly what you would hope. Without it, comparing `bobby.lessons == sandy.lessons` would evaluate to false because it
|
|
63
|
+
# would be comparing the QueryProxy objects, not the lessons themselves.
|
|
64
|
+
def ==(other)
|
|
65
|
+
self.to_a == other
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# For getting variables which have been defined as part of the association chain
|
|
69
|
+
def pluck(*args)
|
|
70
|
+
transformable_attributes = (model ? model.attribute_names : []) + %w(uuid neo_id)
|
|
71
|
+
arg_list = args.map do |arg|
|
|
72
|
+
arg = ActiveGraph::Node::Query::QueryProxy::Link.converted_key(model, arg)
|
|
73
|
+
if transformable_attributes.include?(arg.to_s)
|
|
74
|
+
{identity => arg}
|
|
75
|
+
else
|
|
76
|
+
arg
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
self.query.pluck(*arg_list)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
protected
|
|
84
|
+
|
|
85
|
+
def ensure_distinct(node, force = false)
|
|
86
|
+
@distinct || force ? "DISTINCT(#{node})" : node
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def pluck_vars(node, rel)
|
|
92
|
+
vars = []
|
|
93
|
+
vars << ensure_distinct(identity) if node
|
|
94
|
+
vars << @rel_var if rel
|
|
95
|
+
pluck(*vars)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def set_instance_caches(instance, node, rel)
|
|
99
|
+
instance.each do |object|
|
|
100
|
+
object.instance_variable_set('@source_query_proxy', self)
|
|
101
|
+
object.instance_variable_set('@source_proxy_result_cache', instance)
|
|
102
|
+
if node && rel && object.last.is_a?(ActiveGraph::Relationship)
|
|
103
|
+
object.last.instance_variable_set(association.direction == :in ? '@from_node' : '@to_node', object.first)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module ActiveGraph
|
|
2
|
+
module Node
|
|
3
|
+
module Query
|
|
4
|
+
module QueryProxyFindInBatches
|
|
5
|
+
def find_in_batches(options = {})
|
|
6
|
+
query.return(identity).find_in_batches(identity, @model.primary_key, options) do |batch|
|
|
7
|
+
yield batch.map { |record| record[identity] }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def find_each(options = {})
|
|
12
|
+
query.return(identity).find_each(identity, @model.primary_key, options) do |result|
|
|
13
|
+
yield result[identity]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
module ActiveGraph
|
|
2
|
+
module Node
|
|
3
|
+
module Query
|
|
4
|
+
class QueryProxy
|
|
5
|
+
class Link
|
|
6
|
+
attr_reader :clause
|
|
7
|
+
|
|
8
|
+
def initialize(clause, arg, args = [])
|
|
9
|
+
@clause = clause
|
|
10
|
+
@arg = arg
|
|
11
|
+
@args = args
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def args(var, rel_var)
|
|
15
|
+
if @arg.respond_to?(:call)
|
|
16
|
+
@arg.call(var, rel_var)
|
|
17
|
+
else
|
|
18
|
+
[@arg] + @args
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def for_clause(clause, arg, model, *args)
|
|
24
|
+
method_to_call = "for_#{clause}_clause"
|
|
25
|
+
|
|
26
|
+
send(method_to_call, arg, model, *args)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def for_where_clause(arg, model, *args)
|
|
30
|
+
node_num = 1
|
|
31
|
+
result = []
|
|
32
|
+
if arg.is_a?(Hash)
|
|
33
|
+
arg.each do |key, value|
|
|
34
|
+
if model && model.association?(key)
|
|
35
|
+
result += for_association(key, value, "n#{node_num}", model)
|
|
36
|
+
node_num += 1
|
|
37
|
+
else
|
|
38
|
+
result << new_for_key_and_value(model, key, value)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
elsif arg.is_a?(String)
|
|
42
|
+
result << new(:where, arg, args)
|
|
43
|
+
end
|
|
44
|
+
result
|
|
45
|
+
end
|
|
46
|
+
alias for_node_where_clause for_where_clause
|
|
47
|
+
|
|
48
|
+
def for_where_not_clause(*args)
|
|
49
|
+
for_where_clause(*args).each do |link|
|
|
50
|
+
link.instance_variable_set('@clause', :where_not)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def new_for_key_and_value(model, key, value)
|
|
55
|
+
key = converted_key(model, key)
|
|
56
|
+
|
|
57
|
+
val = if !model
|
|
58
|
+
value
|
|
59
|
+
elsif key == model.id_property_name && value.is_a?(ActiveGraph::Node)
|
|
60
|
+
value.id
|
|
61
|
+
else
|
|
62
|
+
converted_value(model, key, value)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
new(:where, ->(v, _) { {v => {key => val}} })
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def for_association(name, value, n_string, model)
|
|
69
|
+
neo_id = value.try(:neo_id) || value
|
|
70
|
+
fail ArgumentError, "Invalid value for '#{name}' condition" if not neo_id.is_a?(Integer)
|
|
71
|
+
|
|
72
|
+
[
|
|
73
|
+
new(:match, ->(v, _) { "(#{v})#{model.associations[name].arrow_cypher}(#{n_string})" }),
|
|
74
|
+
new(:where, ->(_, _) { {"ID(#{n_string})" => neo_id.to_i} })
|
|
75
|
+
]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# We don't accept strings here. If you want to use a string, just use where.
|
|
79
|
+
def for_rel_where_clause(arg, _, association)
|
|
80
|
+
arg.each_with_object([]) do |(key, value), result|
|
|
81
|
+
rel_class = association.relationship_class if association.relationship_class
|
|
82
|
+
val = rel_class ? converted_value(rel_class, key, value) : value
|
|
83
|
+
result << new(:where, ->(_, rel_var) { {rel_var => {key => val}} })
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def for_rel_where_not_clause(*args)
|
|
88
|
+
for_rel_where_clause(*args).each do |link|
|
|
89
|
+
link.instance_variable_set('@clause', :where_not)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def for_rel_order_clause(arg, _)
|
|
94
|
+
[new(:order, ->(_, v) { arg.is_a?(String) ? arg : {v => arg} })]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def for_order_clause(arg, model)
|
|
98
|
+
[new(:order, ->(v, _) { arg.is_a?(String) ? arg : {v => converted_keys(model, arg)} })]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def for_args(model, clause, args, association = nil)
|
|
102
|
+
if [:where, :where_not].include?(clause) && args[0].is_a?(String) # Better way?
|
|
103
|
+
[for_arg(model, clause, args[0], *args[1..-1])]
|
|
104
|
+
elsif [:rel_where, :rel_where_not].include?(clause)
|
|
105
|
+
args.map { |arg| for_arg(model, clause, arg, association) }
|
|
106
|
+
else
|
|
107
|
+
args.map { |arg| for_arg(model, clause, arg) }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def for_arg(model, clause, arg, *args)
|
|
112
|
+
default = [Link.new(clause, arg, *args)]
|
|
113
|
+
|
|
114
|
+
Link.for_clause(clause, arg, model, *args) || default
|
|
115
|
+
rescue NoMethodError
|
|
116
|
+
default
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def converted_keys(model, arg)
|
|
120
|
+
arg.is_a?(Hash) ? Hash[arg.map { |key, value| [converted_key(model, key), value] }] : arg
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def converted_key(model, key)
|
|
124
|
+
if key.to_sym == :id
|
|
125
|
+
model ? model.id_property_name : :uuid
|
|
126
|
+
else
|
|
127
|
+
key
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def converted_value(model, key, value)
|
|
132
|
+
model.declared_properties.value_for_where(key, value)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
module ActiveGraph
|
|
2
|
+
module Node
|
|
3
|
+
module Query
|
|
4
|
+
# rubocop:disable Metrics/ModuleLength
|
|
5
|
+
module QueryProxyMethods
|
|
6
|
+
# rubocop:enable Metrics/ModuleLength
|
|
7
|
+
FIRST = 'HEAD'
|
|
8
|
+
LAST = 'LAST'
|
|
9
|
+
|
|
10
|
+
def rels
|
|
11
|
+
fail 'Cannot get rels without a relationship variable.' if !@rel_var
|
|
12
|
+
|
|
13
|
+
pluck(@rel_var)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def rel
|
|
17
|
+
rels.first
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def as(node_var)
|
|
21
|
+
new_link(node_var)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Give ability to call `#find` on associations to get a scoped find
|
|
25
|
+
# Doesn't pass through via `method_missing` because Enumerable has a `#find` method
|
|
26
|
+
def find(*args)
|
|
27
|
+
scoping { @model.find(*args) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def first(target = nil)
|
|
31
|
+
first_and_last(FIRST, target)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def last(target = nil)
|
|
35
|
+
first_and_last(LAST, target)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def order_property
|
|
39
|
+
# This should maybe be based on a setting in the association
|
|
40
|
+
# rather than a hardcoded `nil`
|
|
41
|
+
model ? model.id_property_name : nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def distinct
|
|
45
|
+
new_link.tap do |e|
|
|
46
|
+
e.instance_variable_set(:@distinct, true)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def propagate_context(query_proxy)
|
|
51
|
+
query_proxy.instance_variable_set(:@distinct, @distinct)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Integer] number of nodes of this class
|
|
55
|
+
def count(distinct = nil, target = nil)
|
|
56
|
+
return 0 if unpersisted_start_object?
|
|
57
|
+
fail(ActiveGraph::InvalidParameterError, ':count accepts the `:distinct` symbol or nil as a parameter') unless distinct.nil? || distinct == :distinct
|
|
58
|
+
query_with_target(target) do |var|
|
|
59
|
+
q = ensure_distinct(var, !distinct.nil?)
|
|
60
|
+
limited_query = self.query.clause?(:limit) ? self.query.break.with(var) : self.query.reorder
|
|
61
|
+
limited_query.pluck("count(#{q}) AS #{var}").first
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def size
|
|
66
|
+
result_cache? ? result_cache_for.length : count
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
delegate :length, to: :to_a
|
|
70
|
+
|
|
71
|
+
# TODO: update this with public API methods if/when they are exposed
|
|
72
|
+
def limit_value
|
|
73
|
+
return unless self.query.clause?(:limit)
|
|
74
|
+
limit_clause = self.query.send(:clauses).find { |clause| clause.is_a?(ActiveGraph::Core::QueryClauses::LimitClause) }
|
|
75
|
+
limit_clause.instance_variable_get(:@arg)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def empty?(target = nil)
|
|
79
|
+
return true if unpersisted_start_object?
|
|
80
|
+
query_with_target(target) { |var| !self.exists?(nil, var) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
alias blank? empty?
|
|
84
|
+
|
|
85
|
+
# @param [ActiveGraph::Node, ActiveGraph::Node, String] other An instance of a Neo4j.rb model, a core node, or a string uuid
|
|
86
|
+
# @param [String, Symbol] target An identifier of a link in the Cypher chain
|
|
87
|
+
# @return [Boolean]
|
|
88
|
+
def include?(other, target = nil)
|
|
89
|
+
query_with_target(target) do |var|
|
|
90
|
+
where_filter = if other.respond_to?(:neo_id) || association_id_key == :neo_id
|
|
91
|
+
"ID(#{var}) = $other_node_id"
|
|
92
|
+
else
|
|
93
|
+
"#{var}.#{association_id_key} = $other_node_id"
|
|
94
|
+
end
|
|
95
|
+
node_id = other.respond_to?(:neo_id) ? other.neo_id : other
|
|
96
|
+
self.where(where_filter).params(other_node_id: node_id).query.reorder.return("count(#{var}) as count")
|
|
97
|
+
.first[:count].positive?
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def exists?(node_condition = nil, target = nil)
|
|
102
|
+
unless [Integer, String, Hash, NilClass].any? { |c| node_condition.is_a?(c) }
|
|
103
|
+
fail(ActiveGraph::InvalidParameterError, ':exists? only accepts ids or conditions')
|
|
104
|
+
end
|
|
105
|
+
query_with_target(target) do |var|
|
|
106
|
+
start_q = exists_query_start(node_condition, var)
|
|
107
|
+
result = start_q.query.reorder.return("ID(#{var}) AS proof_of_life LIMIT 1").first
|
|
108
|
+
!!result
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Shorthand for `MATCH (start)-[r]-(other_node) WHERE ID(other_node) = #{other_node.neo_id}`
|
|
113
|
+
# The `node` param can be a persisted Node instance, any string or integer, or nil.
|
|
114
|
+
# When it's a node, it'll use the object's neo_id, which is fastest. When not nil, it'll figure out the
|
|
115
|
+
# primary key of that model. When nil, it uses `1 = 2` to prevent matching all records, which is the default
|
|
116
|
+
# behavior when nil is passed to `where` in QueryProxy.
|
|
117
|
+
# @param [#neo_id, String, Enumerable] node A node, a string representing a node's ID, or an enumerable of nodes or IDs.
|
|
118
|
+
# @return [ActiveGraph::Node::Query::QueryProxy] A QueryProxy object upon which you can build.
|
|
119
|
+
def match_to(node)
|
|
120
|
+
first_node = node.is_a?(Array) ? node.first : node
|
|
121
|
+
where_arg = if first_node.respond_to?(:neo_id)
|
|
122
|
+
{neo_id: node.is_a?(Array) ? node.map(&:neo_id) : node}
|
|
123
|
+
elsif !node.nil?
|
|
124
|
+
{association_id_key => node.is_a?(Array) ? ids_array(node) : node}
|
|
125
|
+
else
|
|
126
|
+
# support for null object pattern
|
|
127
|
+
'1 = 2'
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
self.where(where_arg)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# Gives you the first relationship between the last link of a QueryProxy chain and a given node
|
|
135
|
+
# Shorthand for `MATCH (start)-[r]-(other_node) WHERE ID(other_node) = #{other_node.neo_id} RETURN r`
|
|
136
|
+
# @param [#neo_id, String, Enumerable] node An object to be sent to `match_to`. See params for that method.
|
|
137
|
+
# @return A relationship (Relationship, CypherRelationship, EmbeddedRelationship) or nil.
|
|
138
|
+
def first_rel_to(node)
|
|
139
|
+
self.match_to(node).limit(1).pluck(rel_var).first
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Returns all relationships across a QueryProxy chain between a given node or array of nodes and the preceeding link.
|
|
143
|
+
# @param [#neo_id, String, Enumerable] node An object to be sent to `match_to`. See params for that method.
|
|
144
|
+
# @return An enumerable of relationship objects.
|
|
145
|
+
def rels_to(node)
|
|
146
|
+
self.match_to(node).pluck(rel_var)
|
|
147
|
+
end
|
|
148
|
+
alias all_rels_to rels_to
|
|
149
|
+
|
|
150
|
+
# When called, this method returns a single node that satisfies the match specified in the params hash.
|
|
151
|
+
# If no existing node is found to satisfy the match, one is created or associated as expected.
|
|
152
|
+
def find_or_create_by(params)
|
|
153
|
+
fail 'Method invalid when called on Class objects' unless source_object
|
|
154
|
+
result = self.where(params).first
|
|
155
|
+
return result unless result.nil?
|
|
156
|
+
ActiveGraph::Base.transaction do
|
|
157
|
+
node = model.create(params)
|
|
158
|
+
self << node
|
|
159
|
+
node
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def find_or_initialize_by(attributes, &block)
|
|
164
|
+
find_by(attributes) || initialize_by_current_chain_params(attributes, &block)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def first_or_initialize(attributes = {}, &block)
|
|
168
|
+
first || initialize_by_current_chain_params(attributes, &block)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# A shortcut for attaching a new, optional match to the end of a QueryProxy chain.
|
|
172
|
+
def optional(association, node_var = nil, rel_var = nil)
|
|
173
|
+
self.send(association, node_var, rel_var, optional: true)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Takes an Array of Node models and applies the appropriate WHERE clause
|
|
177
|
+
# So for a `Teacher` model inheriting from a `Person` model and an `Article` model
|
|
178
|
+
# if you called .as_models([Teacher, Article])
|
|
179
|
+
# The where clause would look something like:
|
|
180
|
+
#
|
|
181
|
+
# .. code-block:: cypher
|
|
182
|
+
#
|
|
183
|
+
# WHERE (node_var:Teacher:Person OR node_var:Article)
|
|
184
|
+
def as_models(models)
|
|
185
|
+
where_clause = models.map do |model|
|
|
186
|
+
"`#{identity}`:" + model.mapped_label_names.map do |mapped_label_name|
|
|
187
|
+
"`#{mapped_label_name}`"
|
|
188
|
+
end.join(':')
|
|
189
|
+
end.join(' OR ')
|
|
190
|
+
|
|
191
|
+
where("(#{where_clause})")
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Matches all nodes having at least a relation
|
|
195
|
+
#
|
|
196
|
+
# @example Load all people having a friend
|
|
197
|
+
# Person.all.having_rel(:friends).to_a # => Returns a list of `Person`
|
|
198
|
+
#
|
|
199
|
+
# @example Load all people having a best friend
|
|
200
|
+
# Person.all.having_rel(:friends, best: true).to_a # => Returns a list of `Person`
|
|
201
|
+
#
|
|
202
|
+
# @return [QueryProxy] A new QueryProxy
|
|
203
|
+
def having_rel(association_name, rel_properties = {})
|
|
204
|
+
association = association_or_fail(association_name)
|
|
205
|
+
where("(#{identity})#{association.arrow_cypher(nil, rel_properties)}()")
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Matches all nodes not having a certain relation
|
|
209
|
+
#
|
|
210
|
+
# @example Load all people not having friends
|
|
211
|
+
# Person.all.not_having_rel(:friends).to_a # => Returns a list of `Person`
|
|
212
|
+
#
|
|
213
|
+
# @example Load all people not having best friends
|
|
214
|
+
# Person.all.not_having_rel(:friends, best: true).to_a # => Returns a list of `Person`
|
|
215
|
+
#
|
|
216
|
+
# @return [QueryProxy] A new QueryProxy
|
|
217
|
+
def not_having_rel(association_name, rel_properties = {})
|
|
218
|
+
association = association_or_fail(association_name)
|
|
219
|
+
where_not("(#{identity})#{association.arrow_cypher(nil, rel_properties)}()")
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
private
|
|
223
|
+
|
|
224
|
+
def association_or_fail(association_name)
|
|
225
|
+
model.associations[association_name] || fail(ArgumentError, "No such association #{association_name}")
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def find_inverse_association!(model, source, association)
|
|
229
|
+
model.associations.values.find do |reverse_association|
|
|
230
|
+
association.inverse_of?(reverse_association) ||
|
|
231
|
+
reverse_association.inverse_of?(association) ||
|
|
232
|
+
inverse_relation_of?(source, association, model, reverse_association)
|
|
233
|
+
end || fail("Could not find reverse association for #{@context}")
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def inverse_relation_of?(source, source_association, target, target_association)
|
|
237
|
+
source_association.direction != target_association.direction &&
|
|
238
|
+
source == target_association.target_class &&
|
|
239
|
+
target == source_association.target_class &&
|
|
240
|
+
source_association.relationship_class_name == target_association.relationship_class_name
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def initialize_by_current_chain_params(params = {})
|
|
244
|
+
result = new(where_clause_params.merge(params))
|
|
245
|
+
|
|
246
|
+
inverse_association = find_inverse_association!(model, source_object.class, association) if source_object
|
|
247
|
+
result.tap do |m|
|
|
248
|
+
yield(m) if block_given?
|
|
249
|
+
m.public_send(inverse_association.name) << source_object if inverse_association
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def where_clause_params
|
|
254
|
+
query.clauses.select { |c| c.is_a?(ActiveGraph::Core::QueryClauses::WhereClause) && c.arg.is_a?(Hash) }
|
|
255
|
+
.map! { |e| e.arg[identity] }.compact.inject { |a, b| a.merge(b) } || {}
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def first_and_last(func, target)
|
|
259
|
+
new_query, pluck_proc = if self.query.clause?(:order)
|
|
260
|
+
[self.query.with(identity),
|
|
261
|
+
proc { |var| "#{func}(COLLECT(#{var})) as #{var}" }]
|
|
262
|
+
else
|
|
263
|
+
ord_prop = (func == LAST ? {order_property => :DESC} : order_property)
|
|
264
|
+
[self.order(ord_prop).limit(1),
|
|
265
|
+
proc { |var| var }]
|
|
266
|
+
end
|
|
267
|
+
query_with_target(target) do |var|
|
|
268
|
+
final_pluck = pluck_proc.call(var)
|
|
269
|
+
new_query.pluck(final_pluck)
|
|
270
|
+
end.first
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# @return [String] The primary key of a the current QueryProxy's model or target class
|
|
274
|
+
def association_id_key
|
|
275
|
+
self.association.nil? ? model.primary_key : self.association.target_class.primary_key
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# @param [Enumerable] node An enumerable of nodes or ids.
|
|
279
|
+
# @return [Array] An array after having `id` called on each object
|
|
280
|
+
def ids_array(node)
|
|
281
|
+
node.first.respond_to?(:id) ? node.map(&:id) : node
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def query_with_target(target)
|
|
285
|
+
yield(target || identity)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def exists_query_start(condition, target)
|
|
289
|
+
case condition
|
|
290
|
+
when Integer
|
|
291
|
+
self.where("ID(#{target}) = $exists_condition").params(exists_condition: condition)
|
|
292
|
+
when Hash
|
|
293
|
+
self.where(condition.keys.first => condition.values.first)
|
|
294
|
+
when String
|
|
295
|
+
self.where(model.primary_key => condition)
|
|
296
|
+
else
|
|
297
|
+
self
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|