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