neo4j 9.6.2 → 10.0.0.pre.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +0 -13
  3. data/CONTRIBUTORS +4 -0
  4. data/Gemfile +2 -33
  5. data/lib/neo4j.rb +6 -2
  6. data/lib/neo4j/active_base.rb +19 -22
  7. data/lib/neo4j/active_node/has_n.rb +1 -1
  8. data/lib/neo4j/active_node/labels.rb +1 -11
  9. data/lib/neo4j/active_node/node_wrapper.rb +1 -1
  10. data/lib/neo4j/active_node/query/query_proxy_methods_of_mass_updating.rb +1 -1
  11. data/lib/neo4j/active_rel/rel_wrapper.rb +2 -2
  12. data/lib/neo4j/ansi.rb +14 -0
  13. data/lib/neo4j/core.rb +14 -0
  14. data/lib/neo4j/core/connection_failed_error.rb +6 -0
  15. data/lib/neo4j/core/cypher_error.rb +37 -0
  16. data/lib/neo4j/core/driver.rb +83 -0
  17. data/lib/neo4j/core/has_uri.rb +63 -0
  18. data/lib/neo4j/core/instrumentable.rb +36 -0
  19. data/lib/neo4j/core/label.rb +158 -0
  20. data/lib/neo4j/core/logging.rb +44 -0
  21. data/lib/neo4j/core/node.rb +23 -0
  22. data/lib/neo4j/core/querable.rb +88 -0
  23. data/lib/neo4j/core/query.rb +487 -0
  24. data/lib/neo4j/core/query_builder.rb +32 -0
  25. data/lib/neo4j/core/query_clauses.rb +727 -0
  26. data/lib/neo4j/core/query_find_in_batches.rb +49 -0
  27. data/lib/neo4j/core/relationship.rb +13 -0
  28. data/lib/neo4j/core/responses.rb +50 -0
  29. data/lib/neo4j/core/result.rb +33 -0
  30. data/lib/neo4j/core/schema.rb +30 -0
  31. data/lib/neo4j/core/schema_errors.rb +12 -0
  32. data/lib/neo4j/core/wrappable.rb +30 -0
  33. data/lib/neo4j/migration.rb +2 -2
  34. data/lib/neo4j/migrations/base.rb +1 -1
  35. data/lib/neo4j/model_schema.rb +2 -2
  36. data/lib/neo4j/railtie.rb +8 -52
  37. data/lib/neo4j/schema/operation.rb +1 -1
  38. data/lib/neo4j/shared.rb +1 -1
  39. data/lib/neo4j/shared/property.rb +1 -1
  40. data/lib/neo4j/tasks/migration.rake +5 -4
  41. data/lib/neo4j/transaction.rb +137 -0
  42. data/lib/neo4j/version.rb +1 -1
  43. data/neo4j.gemspec +5 -5
  44. metadata +59 -26
  45. data/bin/neo4j-jars +0 -33
  46. data/lib/neo4j/active_base/session_registry.rb +0 -12
  47. 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