neo4j 9.6.2 → 10.0.0.pre.alpha.1
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 +4 -4
- data/CHANGELOG.md +0 -13
- data/CONTRIBUTORS +4 -0
- data/Gemfile +2 -33
- data/lib/neo4j.rb +6 -2
- data/lib/neo4j/active_base.rb +19 -22
- data/lib/neo4j/active_node/has_n.rb +1 -1
- data/lib/neo4j/active_node/labels.rb +1 -11
- data/lib/neo4j/active_node/node_wrapper.rb +1 -1
- data/lib/neo4j/active_node/query/query_proxy_methods_of_mass_updating.rb +1 -1
- data/lib/neo4j/active_rel/rel_wrapper.rb +2 -2
- data/lib/neo4j/ansi.rb +14 -0
- data/lib/neo4j/core.rb +14 -0
- data/lib/neo4j/core/connection_failed_error.rb +6 -0
- data/lib/neo4j/core/cypher_error.rb +37 -0
- data/lib/neo4j/core/driver.rb +83 -0
- data/lib/neo4j/core/has_uri.rb +63 -0
- data/lib/neo4j/core/instrumentable.rb +36 -0
- data/lib/neo4j/core/label.rb +158 -0
- data/lib/neo4j/core/logging.rb +44 -0
- data/lib/neo4j/core/node.rb +23 -0
- data/lib/neo4j/core/querable.rb +88 -0
- data/lib/neo4j/core/query.rb +487 -0
- data/lib/neo4j/core/query_builder.rb +32 -0
- data/lib/neo4j/core/query_clauses.rb +727 -0
- data/lib/neo4j/core/query_find_in_batches.rb +49 -0
- data/lib/neo4j/core/relationship.rb +13 -0
- data/lib/neo4j/core/responses.rb +50 -0
- data/lib/neo4j/core/result.rb +33 -0
- data/lib/neo4j/core/schema.rb +30 -0
- data/lib/neo4j/core/schema_errors.rb +12 -0
- data/lib/neo4j/core/wrappable.rb +30 -0
- data/lib/neo4j/migration.rb +2 -2
- data/lib/neo4j/migrations/base.rb +1 -1
- data/lib/neo4j/model_schema.rb +2 -2
- data/lib/neo4j/railtie.rb +8 -52
- data/lib/neo4j/schema/operation.rb +1 -1
- data/lib/neo4j/shared.rb +1 -1
- data/lib/neo4j/shared/property.rb +1 -1
- data/lib/neo4j/tasks/migration.rake +5 -4
- data/lib/neo4j/transaction.rb +137 -0
- data/lib/neo4j/version.rb +1 -1
- data/neo4j.gemspec +5 -5
- metadata +59 -26
- data/bin/neo4j-jars +0 -33
- data/lib/neo4j/active_base/session_registry.rb +0 -12
- data/lib/neo4j/session_manager.rb +0 -78
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Neo4j
|
4
|
+
module Core
|
5
|
+
# Containing the logic for dealing with adaptors which use URIs
|
6
|
+
module HasUri
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
%w[scheme user password host port].each do |method|
|
11
|
+
define_method(method) do
|
12
|
+
(@uri && @uri.send(method)) || (self.class.default_uri && self.class.default_uri.send(method))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
attr_reader :default_uri
|
19
|
+
|
20
|
+
def default_url(default_url)
|
21
|
+
@default_uri = uri_from_url!(default_url)
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate_uri(&block)
|
25
|
+
@uri_validator = block
|
26
|
+
end
|
27
|
+
|
28
|
+
def uri_from_url!(url)
|
29
|
+
validate_url!(url)
|
30
|
+
|
31
|
+
@uri = url.nil? ? @default_uri : URI(url)
|
32
|
+
|
33
|
+
fail ArgumentError, "Invalid URL: #{url.inspect}" if uri_valid?(@uri)
|
34
|
+
|
35
|
+
@uri
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def validate_url!(url)
|
41
|
+
fail ArgumentError, "Invalid URL: #{url.inspect}" if !(url.is_a?(String) || url.nil?)
|
42
|
+
fail ArgumentError, 'No URL or default URL specified' if url.nil? && @default_uri.nil?
|
43
|
+
end
|
44
|
+
|
45
|
+
def uri_valid?(uri)
|
46
|
+
@uri_validator && !@uri_validator.call(uri)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def url
|
51
|
+
@uri.to_s
|
52
|
+
end
|
53
|
+
|
54
|
+
def url=(url)
|
55
|
+
@uri = self.class.uri_from_url!(url)
|
56
|
+
end
|
57
|
+
|
58
|
+
def url_without_password
|
59
|
+
@url_without_password ||= "#{scheme}://#{user + ':...@' if user}#{host}:#{port}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_support/notifications'
|
3
|
+
require 'neo4j/ansi'
|
4
|
+
|
5
|
+
module Neo4j
|
6
|
+
module Core
|
7
|
+
module Instrumentable
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
EMPTY = ''
|
11
|
+
NEWLINE_W_SPACES = "\n "
|
12
|
+
|
13
|
+
class_methods do
|
14
|
+
def subscribe_to_request
|
15
|
+
ActiveSupport::Notifications.subscribe('neo4j.core.bolt.request') do |_, start, finish, _id, _payload|
|
16
|
+
ms = (finish - start) * 1000
|
17
|
+
yield " #{ANSI::BLUE}BOLT:#{ANSI::CLEAR} #{ANSI::YELLOW}#{ms.round}ms#{ANSI::CLEAR} #{Driver.singleton.url_without_password}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def subscribe_to_query
|
22
|
+
ActiveSupport::Notifications.subscribe('neo4j.core.cypher_query') do |_, _start, _finish, _id, payload|
|
23
|
+
query = payload[:query]
|
24
|
+
params_string = (query.parameters && !query.parameters.empty? ? "| #{query.parameters.inspect}" : EMPTY)
|
25
|
+
cypher = query.pretty_cypher ? (NEWLINE_W_SPACES if query.pretty_cypher.include?("\n")).to_s + query.pretty_cypher.gsub(/\n/, NEWLINE_W_SPACES) : query.cypher
|
26
|
+
|
27
|
+
source_line, line_number = Logging.first_external_path_and_line(caller_locations)
|
28
|
+
|
29
|
+
yield " #{ANSI::CYAN}#{query.context || 'CYPHER'}#{ANSI::CLEAR} #{cypher} #{params_string}" +
|
30
|
+
("\n ↳ #{source_line}:#{line_number}" if Driver.singleton.options[:verbose_query_logs] && source_line).to_s
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module Neo4j
|
2
|
+
module Core
|
3
|
+
class Label
|
4
|
+
attr_reader :name
|
5
|
+
|
6
|
+
def initialize(name)
|
7
|
+
@name = name
|
8
|
+
end
|
9
|
+
|
10
|
+
def create_index(property, options = {})
|
11
|
+
validate_index_options!(options)
|
12
|
+
properties = property.is_a?(Array) ? property.join(',') : property
|
13
|
+
schema_query("CREATE INDEX ON :`#{@name}`(#{properties})")
|
14
|
+
end
|
15
|
+
|
16
|
+
def drop_index(property, options = {})
|
17
|
+
validate_index_options!(options)
|
18
|
+
schema_query("DROP INDEX ON :`#{@name}`(#{property})")
|
19
|
+
end
|
20
|
+
|
21
|
+
# Creates a neo4j constraint on a property
|
22
|
+
# See http://docs.neo4j.org/chunked/stable/query-constraints.html
|
23
|
+
# @example
|
24
|
+
# label = Neo4j::Label.create(:person, session)
|
25
|
+
# label.create_constraint(:name, {type: :unique}, session)
|
26
|
+
#
|
27
|
+
def create_constraint(property, constraints)
|
28
|
+
cypher = case constraints[:type]
|
29
|
+
when :unique, :uniqueness
|
30
|
+
"CREATE CONSTRAINT ON (n:`#{name}`) ASSERT n.`#{property}` IS UNIQUE"
|
31
|
+
else
|
32
|
+
fail "Not supported constraint #{constraints.inspect} for property #{property} (expected :type => :unique)"
|
33
|
+
end
|
34
|
+
schema_query(cypher)
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_uniqueness_constraint(property, options = {})
|
38
|
+
create_constraint(property, options.merge(type: :unique))
|
39
|
+
end
|
40
|
+
|
41
|
+
# Drops a neo4j constraint on a property
|
42
|
+
# See http://docs.neo4j.org/chunked/stable/query-constraints.html
|
43
|
+
# @example
|
44
|
+
# label = Neo4j::Label.create(:person, session)
|
45
|
+
# label.create_constraint(:name, {type: :unique}, session)
|
46
|
+
# label.drop_constraint(:name, {type: :unique}, session)
|
47
|
+
#
|
48
|
+
def drop_constraint(property, constraint)
|
49
|
+
cypher = case constraint[:type]
|
50
|
+
when :unique, :uniqueness
|
51
|
+
"DROP CONSTRAINT ON (n:`#{name}`) ASSERT n.`#{property}` IS UNIQUE"
|
52
|
+
else
|
53
|
+
fail "Not supported constraint #{constraint.inspect}"
|
54
|
+
end
|
55
|
+
schema_query(cypher)
|
56
|
+
end
|
57
|
+
|
58
|
+
def drop_uniqueness_constraint(property, options = {})
|
59
|
+
drop_constraint(property, options.merge(type: :unique))
|
60
|
+
end
|
61
|
+
|
62
|
+
def indexes
|
63
|
+
self.class.indexes.select do |definition|
|
64
|
+
definition[:label] == @name.to_sym
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.indexes
|
69
|
+
Neo4j::Transaction.indexes
|
70
|
+
end
|
71
|
+
|
72
|
+
def drop_indexes
|
73
|
+
self.class.drop_indexes
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.drop_indexes
|
77
|
+
indexes.each do |definition|
|
78
|
+
begin
|
79
|
+
Neo4j::Transaction.query("DROP INDEX ON :`#{definition[:label]}`(#{definition[:properties][0]})")
|
80
|
+
rescue Neo4j::Server::CypherResponse::ResponseError
|
81
|
+
# This will error on each constraint. Ignore and continue.
|
82
|
+
next
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def index?(property)
|
88
|
+
indexes.any? { |definition| definition[:properties] == [property.to_sym] }
|
89
|
+
end
|
90
|
+
|
91
|
+
def constraints(_options = {})
|
92
|
+
Neo4j::Transaction.constraints.select do |definition|
|
93
|
+
definition[:label] == @name.to_sym
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def uniqueness_constraints(_options = {})
|
98
|
+
constraints.select do |definition|
|
99
|
+
definition[:type] == :uniqueness
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def drop_uniqueness_constraints
|
104
|
+
self.class.drop_uniqueness_constraints
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.drop_uniqueness_constraints
|
108
|
+
Neo4j::Transaction.constraints.each do |definition|
|
109
|
+
Neo4j::Transaction.query("DROP CONSTRAINT ON (n:`#{definition[:label]}`) ASSERT n.`#{definition[:properties][0]}` IS UNIQUE")
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def constraint?(property)
|
114
|
+
constraints.any? { |definition| definition[:properties] == [property.to_sym] }
|
115
|
+
end
|
116
|
+
|
117
|
+
def uniqueness_constraint?(property)
|
118
|
+
uniqueness_constraints.include?([property])
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.wait_for_schema_changes
|
122
|
+
schema_threads.map(&:join)
|
123
|
+
set_schema_threads(session, [])
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
# Store schema threads on the session so that we can easily wait for all
|
129
|
+
# threads on a session regardless of label
|
130
|
+
def schema_threads
|
131
|
+
self.class.schema_threads
|
132
|
+
end
|
133
|
+
|
134
|
+
def schema_threads=(array)
|
135
|
+
self.class.set_schema_threads(array)
|
136
|
+
end
|
137
|
+
|
138
|
+
class << self
|
139
|
+
def schema_threads
|
140
|
+
Neo4j::Transaction.instance_variable_get('@_schema_threads') || []
|
141
|
+
end
|
142
|
+
|
143
|
+
def set_schema_threads(array)
|
144
|
+
Neo4j::Transaction.instance_variable_set('@_schema_threads', array)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def schema_query(cypher)
|
149
|
+
Neo4j::Transaction.transaction { |tx| tx.query(cypher, {}) }
|
150
|
+
end
|
151
|
+
|
152
|
+
def validate_index_options!(options)
|
153
|
+
return unless options[:type] && options[:type] != :exact
|
154
|
+
fail "Type #{options[:type]} is not supported"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# Copied largely from activerecord/lib/active_record/log_subscriber.rb
|
2
|
+
module Neo4j
|
3
|
+
module Core
|
4
|
+
module Logging
|
5
|
+
class << self
|
6
|
+
def first_external_path_and_line(callstack)
|
7
|
+
line = callstack.find do |frame|
|
8
|
+
frame.absolute_path && !ignored_callstack(frame.absolute_path)
|
9
|
+
end
|
10
|
+
|
11
|
+
offending_line = line || callstack.first
|
12
|
+
|
13
|
+
[offending_line.path,
|
14
|
+
offending_line.lineno]
|
15
|
+
end
|
16
|
+
|
17
|
+
NEO4J_CORE_GEM_ROOT = File.expand_path('../../..', __dir__) + '/'
|
18
|
+
|
19
|
+
def ignored_callstack(path)
|
20
|
+
paths_to_ignore.any?(&path.method(:start_with?))
|
21
|
+
end
|
22
|
+
|
23
|
+
def paths_to_ignore
|
24
|
+
@paths_to_ignore ||= [NEO4J_CORE_GEM_ROOT,
|
25
|
+
RbConfig::CONFIG['rubylibdir'],
|
26
|
+
neo4j_gem_path,
|
27
|
+
active_support_gem_path].compact
|
28
|
+
end
|
29
|
+
|
30
|
+
def neo4j_gem_path
|
31
|
+
return if !defined?(::Rails.root)
|
32
|
+
|
33
|
+
@neo4j_gem_path ||= File.expand_path('../../..', Neo4j::ActiveBase.method(:current_driver).source_location[0])
|
34
|
+
end
|
35
|
+
|
36
|
+
def active_support_gem_path
|
37
|
+
return if !defined?(::ActiveSupport::Notifications)
|
38
|
+
|
39
|
+
@active_support_gem_path ||= File.expand_path('../../..', ActiveSupport::Notifications.method(:subscribe).source_location[0])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'neo4j/core/wrappable'
|
2
|
+
|
3
|
+
module Neo4j
|
4
|
+
module Core
|
5
|
+
module Node
|
6
|
+
def props; properties; end
|
7
|
+
# Perhaps we should deprecate this?
|
8
|
+
def neo_id; id; end
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
other.is_a?(Node) && neo_id == other.neo_id
|
12
|
+
end
|
13
|
+
|
14
|
+
def labels
|
15
|
+
@labels ||= super
|
16
|
+
end
|
17
|
+
|
18
|
+
def properties
|
19
|
+
@properties ||= super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'neo4j/core/instrumentable'
|
2
|
+
require 'neo4j/transaction'
|
3
|
+
require 'neo4j/core/query_builder'
|
4
|
+
require 'neo4j/core/responses'
|
5
|
+
|
6
|
+
module Neo4j
|
7
|
+
module Core
|
8
|
+
module Querable
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
include Instrumentable
|
11
|
+
include Responses
|
12
|
+
|
13
|
+
class_methods do
|
14
|
+
def query(*args)
|
15
|
+
options = case args.size
|
16
|
+
when 3
|
17
|
+
args.pop
|
18
|
+
when 2
|
19
|
+
args.pop if args[0].is_a?(::Neo4j::Core::Query)
|
20
|
+
end || {}
|
21
|
+
|
22
|
+
queries(options) { append(*args) }[0]
|
23
|
+
end
|
24
|
+
|
25
|
+
def queries(options = {}, &block)
|
26
|
+
query_builder = QueryBuilder.new
|
27
|
+
|
28
|
+
query_builder.instance_eval(&block)
|
29
|
+
|
30
|
+
new_or_current_transaction(options[:transaction]) do |tx|
|
31
|
+
query_set(tx, query_builder.queries, { commit: !options[:transaction] }.merge(options))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# If called without a block, returns a Transaction object
|
36
|
+
# which can be used to call query/queries/mark_failed/commit
|
37
|
+
# If called with a block, the Transaction object is yielded
|
38
|
+
# to the block and `commit` is ensured. Any uncaught exceptions
|
39
|
+
# will mark the transaction as failed first
|
40
|
+
def transaction
|
41
|
+
return Transaction.new unless block_given?
|
42
|
+
|
43
|
+
begin
|
44
|
+
tx = transaction
|
45
|
+
|
46
|
+
yield tx
|
47
|
+
rescue => e
|
48
|
+
tx.mark_failed if tx
|
49
|
+
|
50
|
+
raise e
|
51
|
+
ensure
|
52
|
+
tx.close if tx
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def setup_queries!(queries, options = {})
|
57
|
+
return if options[:skip_instrumentation]
|
58
|
+
queries.each do |query|
|
59
|
+
ActiveSupport::Notifications.instrument('neo4j.core.cypher_query', query: query)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def query_set(transaction, queries, options = {})
|
64
|
+
setup_queries!(queries, skip_instrumentation: options[:skip_instrumentation])
|
65
|
+
|
66
|
+
ActiveSupport::Notifications.instrument('neo4j.core.bolt.request') do
|
67
|
+
self.wrap_level = options[:wrap_level]
|
68
|
+
queries.map do |query|
|
69
|
+
result_from_data(transaction.root_tx.run(query.cypher, query.parameters))
|
70
|
+
end
|
71
|
+
rescue Neo4j::Driver::Exceptions::Neo4jException => e
|
72
|
+
raise Neo4j::Core::CypherError.new_from(e.code, e.message) # , e.stack_track.to_a
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def new_or_current_transaction(tx, &block)
|
79
|
+
if tx
|
80
|
+
yield(tx)
|
81
|
+
else
|
82
|
+
transaction(&block)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,487 @@
|
|
1
|
+
require 'neo4j/core/query_clauses'
|
2
|
+
require 'neo4j/core/query_find_in_batches'
|
3
|
+
require 'active_support/notifications'
|
4
|
+
|
5
|
+
module Neo4j
|
6
|
+
module Core
|
7
|
+
# Allows for generation of cypher queries via ruby method calls (inspired by ActiveRecord / arel syntax)
|
8
|
+
#
|
9
|
+
# Can be used to express cypher queries in ruby nicely, or to more easily generate queries programatically.
|
10
|
+
#
|
11
|
+
# Also, queries can be passed around an application to progressively build a query across different concerns
|
12
|
+
#
|
13
|
+
# See also the following link for full cypher language documentation:
|
14
|
+
# http://docs.neo4j.org/chunked/milestone/cypher-query-lang.html
|
15
|
+
class Query
|
16
|
+
include Neo4j::Core::QueryClauses
|
17
|
+
include Neo4j::Core::QueryFindInBatches
|
18
|
+
DEFINED_CLAUSES = {}
|
19
|
+
|
20
|
+
|
21
|
+
attr_accessor :clauses
|
22
|
+
|
23
|
+
class Parameters
|
24
|
+
def initialize(hash = nil)
|
25
|
+
@parameters = (hash || {})
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_hash
|
29
|
+
@parameters
|
30
|
+
end
|
31
|
+
|
32
|
+
def copy
|
33
|
+
self.class.new(@parameters.dup)
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_param(key, value)
|
37
|
+
free_param_key(key).tap do |k|
|
38
|
+
@parameters[k.freeze] = value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def remove_param(key)
|
43
|
+
@parameters.delete(key.to_sym)
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_params(params)
|
47
|
+
params.map do |key, value|
|
48
|
+
add_param(key, value)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def free_param_key(key)
|
55
|
+
k = key.to_sym
|
56
|
+
|
57
|
+
return k if !@parameters.key?(k)
|
58
|
+
|
59
|
+
i = 2
|
60
|
+
i += 1 while @parameters.key?("#{key}#{i}".to_sym)
|
61
|
+
|
62
|
+
"#{key}#{i}".to_sym
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class << self
|
67
|
+
attr_accessor :pretty_cypher
|
68
|
+
end
|
69
|
+
|
70
|
+
def initialize(options = {})
|
71
|
+
@session = options[:session]
|
72
|
+
|
73
|
+
@options = options
|
74
|
+
@clauses = []
|
75
|
+
@_params = {}
|
76
|
+
@params = Parameters.new
|
77
|
+
end
|
78
|
+
|
79
|
+
def inspect
|
80
|
+
"#<Query CYPHER: #{ANSI::YELLOW}#{to_cypher.inspect}#{ANSI::CLEAR}>"
|
81
|
+
end
|
82
|
+
|
83
|
+
# @method start *args
|
84
|
+
# START clause
|
85
|
+
# @return [Query]
|
86
|
+
|
87
|
+
# @method match *args
|
88
|
+
# MATCH clause
|
89
|
+
# @return [Query]
|
90
|
+
|
91
|
+
# @method optional_match *args
|
92
|
+
# OPTIONAL MATCH clause
|
93
|
+
# @return [Query]
|
94
|
+
|
95
|
+
# @method using *args
|
96
|
+
# USING clause
|
97
|
+
# @return [Query]
|
98
|
+
|
99
|
+
# @method where *args
|
100
|
+
# WHERE clause
|
101
|
+
# @return [Query]
|
102
|
+
|
103
|
+
# @method with *args
|
104
|
+
# WITH clause
|
105
|
+
# @return [Query]
|
106
|
+
|
107
|
+
# @method with_distinct *args
|
108
|
+
# WITH clause with DISTINCT specified
|
109
|
+
# @return [Query]
|
110
|
+
|
111
|
+
# @method order *args
|
112
|
+
# ORDER BY clause
|
113
|
+
# @return [Query]
|
114
|
+
|
115
|
+
# @method limit *args
|
116
|
+
# LIMIT clause
|
117
|
+
# @return [Query]
|
118
|
+
|
119
|
+
# @method skip *args
|
120
|
+
# SKIP clause
|
121
|
+
# @return [Query]
|
122
|
+
|
123
|
+
# @method set *args
|
124
|
+
# SET clause
|
125
|
+
# @return [Query]
|
126
|
+
|
127
|
+
# @method remove *args
|
128
|
+
# REMOVE clause
|
129
|
+
# @return [Query]
|
130
|
+
|
131
|
+
# @method unwind *args
|
132
|
+
# UNWIND clause
|
133
|
+
# @return [Query]
|
134
|
+
|
135
|
+
# @method return *args
|
136
|
+
# RETURN clause
|
137
|
+
# @return [Query]
|
138
|
+
|
139
|
+
# @method create *args
|
140
|
+
# CREATE clause
|
141
|
+
# @return [Query]
|
142
|
+
|
143
|
+
# @method create_unique *args
|
144
|
+
# CREATE UNIQUE clause
|
145
|
+
# @return [Query]
|
146
|
+
|
147
|
+
# @method merge *args
|
148
|
+
# MERGE clause
|
149
|
+
# @return [Query]
|
150
|
+
|
151
|
+
# @method on_create_set *args
|
152
|
+
# ON CREATE SET clause
|
153
|
+
# @return [Query]
|
154
|
+
|
155
|
+
# @method on_match_set *args
|
156
|
+
# ON MATCH SET clause
|
157
|
+
# @return [Query]
|
158
|
+
|
159
|
+
# @method delete *args
|
160
|
+
# DELETE clause
|
161
|
+
# @return [Query]
|
162
|
+
|
163
|
+
# @method detach_delete *args
|
164
|
+
# DETACH DELETE clause
|
165
|
+
# @return [Query]
|
166
|
+
|
167
|
+
METHODS = %w[start match optional_match call using where create create_unique merge set on_create_set on_match_set remove unwind delete detach_delete with with_distinct return order skip limit] # rubocop:disable Metrics/LineLength
|
168
|
+
BREAK_METHODS = %(with with_distinct call)
|
169
|
+
|
170
|
+
CLAUSIFY_CLAUSE = proc { |method| const_get(method.to_s.split('_').map(&:capitalize).join + 'Clause') }
|
171
|
+
CLAUSES = METHODS.map(&CLAUSIFY_CLAUSE)
|
172
|
+
|
173
|
+
METHODS.each_with_index do |clause, i|
|
174
|
+
clause_class = CLAUSES[i]
|
175
|
+
|
176
|
+
DEFINED_CLAUSES[clause.to_sym] = clause_class
|
177
|
+
define_method(clause) do |*args|
|
178
|
+
result = build_deeper_query(clause_class, args)
|
179
|
+
|
180
|
+
BREAK_METHODS.include?(clause) ? result.break : result
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
alias offset skip
|
185
|
+
alias order_by order
|
186
|
+
|
187
|
+
# Clears out previous order clauses and allows only for those specified by args
|
188
|
+
def reorder(*args)
|
189
|
+
query = copy
|
190
|
+
|
191
|
+
query.remove_clause_class(OrderClause)
|
192
|
+
query.order(*args)
|
193
|
+
end
|
194
|
+
|
195
|
+
# Works the same as the #where method, but the clause is surrounded by a
|
196
|
+
# Cypher NOT() function
|
197
|
+
def where_not(*args)
|
198
|
+
build_deeper_query(WhereClause, args, not: true)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Works the same as the #set method, but when given a nested array it will set properties rather than setting entire objects
|
202
|
+
# @example
|
203
|
+
# # Creates a query representing the cypher: MATCH (n:Person) SET n.age = 19
|
204
|
+
# Query.new.match(n: :Person).set_props(n: {age: 19})
|
205
|
+
def set_props(*args) # rubocop:disable Naming/AccessorMethodName
|
206
|
+
build_deeper_query(SetClause, args, set_props: true)
|
207
|
+
end
|
208
|
+
|
209
|
+
# Allows what's been built of the query so far to be frozen and the rest built anew. Can be called multiple times in a string of method calls
|
210
|
+
# @example
|
211
|
+
# # Creates a query representing the cypher: MATCH (q:Person), r:Car MATCH (p: Person)-->q
|
212
|
+
# Query.new.match(q: Person).match('r:Car').break.match('(p: Person)-->q')
|
213
|
+
def break
|
214
|
+
build_deeper_query(nil)
|
215
|
+
end
|
216
|
+
|
217
|
+
# Allows for the specification of values for params specified in query
|
218
|
+
# @example
|
219
|
+
# # Creates a query representing the cypher: MATCH (q: Person {id: {id}})
|
220
|
+
# # Calls to params don't affect the cypher query generated, but the params will be
|
221
|
+
# # Passed down when the query is made
|
222
|
+
# Query.new.match('(q: Person {id: {id}})').params(id: 12)
|
223
|
+
#
|
224
|
+
def params(args)
|
225
|
+
copy.tap { |new_query| new_query.instance_variable_get('@params'.freeze).add_params(args) }
|
226
|
+
end
|
227
|
+
|
228
|
+
def unwrapped
|
229
|
+
@_unwrapped_obj = true
|
230
|
+
self
|
231
|
+
end
|
232
|
+
|
233
|
+
def unwrapped?
|
234
|
+
!!@_unwrapped_obj
|
235
|
+
end
|
236
|
+
|
237
|
+
def response
|
238
|
+
return @response if @response
|
239
|
+
|
240
|
+
@response = Neo4j::Transaction.query(self, transaction: Transaction.root, wrap_level: (:core_entity if unwrapped?))
|
241
|
+
end
|
242
|
+
|
243
|
+
def raise_if_cypher_error!(response)
|
244
|
+
response.raise_cypher_error if response.respond_to?(:error?) && response.error?
|
245
|
+
end
|
246
|
+
|
247
|
+
def match_nodes(hash, optional_match = false)
|
248
|
+
hash.inject(self) do |query, (variable, node_object)|
|
249
|
+
neo_id = (node_object.respond_to?(:neo_id) ? node_object.neo_id : node_object)
|
250
|
+
|
251
|
+
match_method = optional_match ? :optional_match : :match
|
252
|
+
query.send(match_method, variable).where(variable => {neo_id: neo_id})
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def optional_match_nodes(hash)
|
257
|
+
match_nodes(hash, true)
|
258
|
+
end
|
259
|
+
|
260
|
+
include Enumerable
|
261
|
+
|
262
|
+
def count(var = nil)
|
263
|
+
v = var.nil? ? '*' : var
|
264
|
+
pluck("count(#{v})").first
|
265
|
+
end
|
266
|
+
|
267
|
+
def each
|
268
|
+
response.each { |object| yield object }
|
269
|
+
end
|
270
|
+
|
271
|
+
# @method to_a
|
272
|
+
# Class is Enumerable. Each yield is a Hash with the key matching the variable returned and the value being the value for that key from the response
|
273
|
+
# @return [Array]
|
274
|
+
# @raise [Neo4j::Server::CypherResponse::ResponseError] Raises errors from neo4j server
|
275
|
+
|
276
|
+
|
277
|
+
# Executes a query without returning the result
|
278
|
+
# @return [Boolean] true if successful
|
279
|
+
# @raise [Neo4j::Server::CypherResponse::ResponseError] Raises errors from neo4j server
|
280
|
+
def exec
|
281
|
+
response
|
282
|
+
|
283
|
+
true
|
284
|
+
end
|
285
|
+
|
286
|
+
# Return the specified columns as an array.
|
287
|
+
# If one column is specified, a one-dimensional array is returned with the values of that column
|
288
|
+
# If two columns are specified, a n-dimensional array is returned with the values of those columns
|
289
|
+
#
|
290
|
+
# @example
|
291
|
+
# Query.new.match(n: :Person).return(p: :name}.pluck(p: :name) # => Array of names
|
292
|
+
# @example
|
293
|
+
# Query.new.match(n: :Person).return(p: :name}.pluck('p, DISTINCT p.name') # => Array of [node, name] pairs
|
294
|
+
#
|
295
|
+
def pluck(*columns)
|
296
|
+
fail ArgumentError, 'No columns specified for Query#pluck' if columns.size.zero?
|
297
|
+
|
298
|
+
query = return_query(columns)
|
299
|
+
columns = query.response.columns
|
300
|
+
|
301
|
+
if columns.size == 1
|
302
|
+
column = columns[0]
|
303
|
+
query.map { |row| row[column] }
|
304
|
+
else
|
305
|
+
query.map { |row| columns.map { |column| row[column] } }
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
def return_query(columns)
|
310
|
+
query = copy
|
311
|
+
query.remove_clause_class(ReturnClause)
|
312
|
+
|
313
|
+
query.return(*columns)
|
314
|
+
end
|
315
|
+
|
316
|
+
# Returns a CYPHER query string from the object query representation
|
317
|
+
# @example
|
318
|
+
# Query.new.match(p: :Person).where(p: {age: 30}) # => "MATCH (p:Person) WHERE p.age = 30
|
319
|
+
#
|
320
|
+
# @return [String] Resulting cypher query string
|
321
|
+
EMPTY = ' '
|
322
|
+
NEWLINE = "\n"
|
323
|
+
def to_cypher(options = {})
|
324
|
+
join_string = options[:pretty] ? NEWLINE : EMPTY
|
325
|
+
|
326
|
+
cypher_string = partitioned_clauses.map do |clauses|
|
327
|
+
clauses_by_class = clauses.group_by(&:class)
|
328
|
+
|
329
|
+
cypher_parts = CLAUSES.map do |clause_class|
|
330
|
+
clause_class.to_cypher(clauses, options[:pretty]) if clauses = clauses_by_class[clause_class]
|
331
|
+
end.compact
|
332
|
+
|
333
|
+
cypher_parts.join(join_string).tap(&:strip!)
|
334
|
+
end.join(join_string)
|
335
|
+
|
336
|
+
cypher_string = "CYPHER #{@options[:parser]} #{cypher_string}" if @options[:parser]
|
337
|
+
cypher_string.tap(&:strip!)
|
338
|
+
end
|
339
|
+
alias cypher to_cypher
|
340
|
+
|
341
|
+
def pretty_cypher
|
342
|
+
to_cypher(pretty: true)
|
343
|
+
end
|
344
|
+
|
345
|
+
def context
|
346
|
+
@options[:context]
|
347
|
+
end
|
348
|
+
|
349
|
+
def parameters
|
350
|
+
to_cypher
|
351
|
+
merge_params
|
352
|
+
end
|
353
|
+
|
354
|
+
def partitioned_clauses
|
355
|
+
@partitioned_clauses ||= PartitionedClauses.new(@clauses)
|
356
|
+
end
|
357
|
+
|
358
|
+
def print_cypher
|
359
|
+
puts to_cypher(pretty: true).gsub(/\e[^m]+m/, '')
|
360
|
+
end
|
361
|
+
|
362
|
+
# Returns a CYPHER query specifying the union of the callee object's query and the argument's query
|
363
|
+
#
|
364
|
+
# @example
|
365
|
+
# # Generates cypher: MATCH (n:Person) UNION MATCH (o:Person) WHERE o.age = 10
|
366
|
+
# q = Neo4j::Core::Query.new.match(o: :Person).where(o: {age: 10})
|
367
|
+
# result = Neo4j::Core::Query.new.match(n: :Person).union_cypher(q)
|
368
|
+
#
|
369
|
+
# @param other [Query] Second half of UNION
|
370
|
+
# @param options [Hash] Specify {all: true} to use UNION ALL
|
371
|
+
# @return [String] Resulting UNION cypher query string
|
372
|
+
def union_cypher(other, options = {})
|
373
|
+
"#{to_cypher} UNION#{options[:all] ? ' ALL' : ''} #{other.to_cypher}"
|
374
|
+
end
|
375
|
+
|
376
|
+
def &(other)
|
377
|
+
self.class.new(session: @session).tap do |new_query|
|
378
|
+
new_query.options = options.merge(other.options)
|
379
|
+
new_query.clauses = clauses + other.clauses
|
380
|
+
end.params(other._params)
|
381
|
+
end
|
382
|
+
|
383
|
+
def copy
|
384
|
+
dup.tap do |query|
|
385
|
+
to_cypher
|
386
|
+
query.instance_variable_set('@params'.freeze, @params.copy)
|
387
|
+
query.instance_variable_set('@partitioned_clauses'.freeze, nil)
|
388
|
+
query.instance_variable_set('@response'.freeze, nil)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
def clause?(method)
|
393
|
+
clause_class = DEFINED_CLAUSES[method] || CLAUSIFY_CLAUSE.call(method)
|
394
|
+
clauses.any? { |clause| clause.is_a?(clause_class) }
|
395
|
+
end
|
396
|
+
|
397
|
+
protected
|
398
|
+
|
399
|
+
attr_accessor :session, :options, :_params
|
400
|
+
|
401
|
+
def add_clauses(clauses)
|
402
|
+
@clauses += clauses
|
403
|
+
end
|
404
|
+
|
405
|
+
def remove_clause_class(clause_class)
|
406
|
+
@clauses = @clauses.reject { |clause| clause.is_a?(clause_class) }
|
407
|
+
end
|
408
|
+
|
409
|
+
private
|
410
|
+
|
411
|
+
def build_deeper_query(clause_class, args = {}, options = {})
|
412
|
+
copy.tap do |new_query|
|
413
|
+
new_query.add_clauses [nil] if [nil, WithClause].include?(clause_class)
|
414
|
+
new_query.add_clauses clause_class.from_args(args, new_query.instance_variable_get('@params'.freeze), options) if clause_class
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
class PartitionedClauses
|
419
|
+
def initialize(clauses)
|
420
|
+
@clauses = clauses
|
421
|
+
@partitioning = [[]]
|
422
|
+
end
|
423
|
+
|
424
|
+
include Enumerable
|
425
|
+
|
426
|
+
def each
|
427
|
+
generate_partitioning!
|
428
|
+
|
429
|
+
@partitioning.each { |partition| yield partition }
|
430
|
+
end
|
431
|
+
|
432
|
+
def generate_partitioning!
|
433
|
+
@partitioning = [[]]
|
434
|
+
|
435
|
+
@clauses.each do |clause|
|
436
|
+
if clause.nil? && !fresh_partition?
|
437
|
+
@partitioning << []
|
438
|
+
elsif clause_is_order_or_limit_directly_following_with_or_order?(clause)
|
439
|
+
second_to_last << clause
|
440
|
+
elsif clause_is_with_following_order_or_limit?(clause)
|
441
|
+
second_to_last << clause
|
442
|
+
second_to_last.sort_by! { |c| c.is_a?(::Neo4j::Core::QueryClauses::OrderClause) ? 1 : 0 }
|
443
|
+
else
|
444
|
+
@partitioning.last << clause
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
private
|
450
|
+
|
451
|
+
def fresh_partition?
|
452
|
+
@partitioning.last == []
|
453
|
+
end
|
454
|
+
|
455
|
+
def second_to_last
|
456
|
+
@partitioning[-2]
|
457
|
+
end
|
458
|
+
|
459
|
+
def clause_is_order_or_limit_directly_following_with_or_order?(clause)
|
460
|
+
self.class.clause_is_order_or_limit?(clause) &&
|
461
|
+
@partitioning[-2] &&
|
462
|
+
@partitioning[-1].empty? &&
|
463
|
+
(@partitioning[-2].last.is_a?(::Neo4j::Core::QueryClauses::WithClause) ||
|
464
|
+
@partitioning[-2].last.is_a?(::Neo4j::Core::QueryClauses::OrderClause))
|
465
|
+
end
|
466
|
+
|
467
|
+
def clause_is_with_following_order_or_limit?(clause)
|
468
|
+
clause.is_a?(::Neo4j::Core::QueryClauses::WithClause) &&
|
469
|
+
@partitioning[-2] && @partitioning[-2].any? { |c| self.class.clause_is_order_or_limit?(c) }
|
470
|
+
end
|
471
|
+
|
472
|
+
class << self
|
473
|
+
def clause_is_order_or_limit?(clause)
|
474
|
+
clause.is_a?(::Neo4j::Core::QueryClauses::OrderClause) ||
|
475
|
+
clause.is_a?(::Neo4j::Core::QueryClauses::LimitClause)
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
# SHOULD BE DEPRECATED
|
481
|
+
def merge_params
|
482
|
+
@merge_params_base ||= @clauses.compact.inject({}) { |params, clause| params.merge!(clause.params) }
|
483
|
+
@params.to_hash.merge(@merge_params_base)
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|