neo4j-core 6.1.6 → 7.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -9
  3. data/README.md +48 -0
  4. data/lib/neo4j-core.rb +23 -0
  5. data/lib/neo4j-core/helpers.rb +8 -0
  6. data/lib/neo4j-core/query.rb +23 -20
  7. data/lib/neo4j-core/query_clauses.rb +18 -32
  8. data/lib/neo4j-core/query_find_in_batches.rb +3 -1
  9. data/lib/neo4j-core/version.rb +1 -1
  10. data/lib/neo4j-embedded/cypher_response.rb +4 -0
  11. data/lib/neo4j-embedded/embedded_database.rb +3 -5
  12. data/lib/neo4j-embedded/embedded_node.rb +4 -4
  13. data/lib/neo4j-embedded/embedded_session.rb +21 -10
  14. data/lib/neo4j-embedded/embedded_transaction.rb +4 -10
  15. data/lib/neo4j-server/cypher_node.rb +5 -4
  16. data/lib/neo4j-server/cypher_relationship.rb +3 -3
  17. data/lib/neo4j-server/cypher_response.rb +4 -0
  18. data/lib/neo4j-server/cypher_session.rb +31 -22
  19. data/lib/neo4j-server/cypher_transaction.rb +23 -15
  20. data/lib/neo4j-server/resource.rb +3 -4
  21. data/lib/neo4j/core/cypher_session.rb +17 -9
  22. data/lib/neo4j/core/cypher_session/adaptors.rb +116 -33
  23. data/lib/neo4j/core/cypher_session/adaptors/bolt.rb +331 -0
  24. data/lib/neo4j/core/cypher_session/adaptors/bolt/chunk_writer_io.rb +76 -0
  25. data/lib/neo4j/core/cypher_session/adaptors/bolt/pack_stream.rb +288 -0
  26. data/lib/neo4j/core/cypher_session/adaptors/embedded.rb +60 -29
  27. data/lib/neo4j/core/cypher_session/adaptors/has_uri.rb +63 -0
  28. data/lib/neo4j/core/cypher_session/adaptors/http.rb +123 -119
  29. data/lib/neo4j/core/cypher_session/responses.rb +17 -2
  30. data/lib/neo4j/core/cypher_session/responses/bolt.rb +135 -0
  31. data/lib/neo4j/core/cypher_session/responses/embedded.rb +46 -11
  32. data/lib/neo4j/core/cypher_session/responses/http.rb +49 -40
  33. data/lib/neo4j/core/cypher_session/transactions.rb +33 -0
  34. data/lib/neo4j/core/cypher_session/transactions/bolt.rb +36 -0
  35. data/lib/neo4j/core/cypher_session/transactions/embedded.rb +32 -0
  36. data/lib/neo4j/core/cypher_session/transactions/http.rb +52 -0
  37. data/lib/neo4j/core/instrumentable.rb +2 -2
  38. data/lib/neo4j/core/label.rb +182 -0
  39. data/lib/neo4j/core/node.rb +8 -3
  40. data/lib/neo4j/core/relationship.rb +12 -4
  41. data/lib/neo4j/entity_equality.rb +1 -1
  42. data/lib/neo4j/session.rb +4 -5
  43. data/lib/neo4j/transaction.rb +108 -72
  44. data/neo4j-core.gemspec +6 -6
  45. metadata +34 -40
@@ -0,0 +1,32 @@
1
+ require 'neo4j/core/cypher_session/transactions'
2
+
3
+ module Neo4j
4
+ module Core
5
+ class CypherSession
6
+ module Transactions
7
+ class Embedded < Base
8
+ def initialize(*args)
9
+ super
10
+ @java_tx = adaptor.graph_db.begin_tx
11
+ end
12
+
13
+ def commit
14
+ return if !@java_tx
15
+
16
+ @java_tx.success
17
+ @java_tx.close
18
+ rescue Java::OrgNeo4jGraphdb::TransactionFailureException => e
19
+ raise CypherError, e.message
20
+ end
21
+
22
+ def delete
23
+ return if !@java_tx
24
+
25
+ @java_tx.failure
26
+ @java_tx.close
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,52 @@
1
+ require 'neo4j/core/cypher_session/transactions'
2
+
3
+ module Neo4j
4
+ module Core
5
+ class CypherSession
6
+ module Transactions
7
+ class HTTP < Base
8
+ # Should perhaps have transaction adaptors only define #close
9
+ # commit/delete are, I think, an implementation detail
10
+
11
+ def commit
12
+ adaptor.requestor.request(:post, query_path(true)) if started?
13
+ end
14
+
15
+ def delete
16
+ adaptor.requestor.request(:delete, query_path) if started?
17
+ end
18
+
19
+ def query_path(commit = false)
20
+ if id
21
+ "/db/data/transaction/#{id}"
22
+ else
23
+ '/db/data/transaction'
24
+ end.tap do |path|
25
+ path << '/commit' if commit
26
+ end
27
+ end
28
+
29
+ # Takes the transaction URL from Neo4j and parses out the ID
30
+ def apply_id_from_url!(url)
31
+ root.instance_variable_set('@id', url.match(%r{/(\d+)/?$})[1].to_i) if url
32
+ # @id = url.match(%r{/(\d+)/?$})[1].to_i if url
33
+ end
34
+
35
+ def started?
36
+ !!id
37
+ end
38
+
39
+ def id
40
+ root.instance_variable_get('@id')
41
+ end
42
+
43
+ private
44
+
45
+ def connection
46
+ adaptor.connection
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -12,13 +12,13 @@ module Neo4j
12
12
  end
13
13
 
14
14
  module ClassMethods
15
- def instrument(name, label, arguments, &block)
15
+ def instrument(name, label, arguments)
16
16
  # defining class methods
17
17
  klass = class << self; self; end
18
18
  klass.instance_eval do
19
19
  define_method("subscribe_to_#{name}") do |&b|
20
20
  ActiveSupport::Notifications.subscribe(label) do |a, start, finish, id, payload|
21
- b.call block.call(a, start, finish, id, payload)
21
+ b.call yield(a, start, finish, id, payload)
22
22
  end
23
23
  end
24
24
 
@@ -0,0 +1,182 @@
1
+ module Neo4j
2
+ module Core
3
+ class Label
4
+ attr_reader :name
5
+
6
+ def initialize(name, session)
7
+ @name = name
8
+ @session = session
9
+ end
10
+
11
+ def create_index(property, options = {})
12
+ validate_index_options!(options)
13
+ properties = property.is_a?(Array) ? property.join(',') : property
14
+ schema_query("CREATE INDEX ON :`#{@name}`(#{properties})")
15
+ end
16
+
17
+ def drop_index(property, options = {})
18
+ validate_index_options!(options)
19
+ schema_query("DROP INDEX ON :`#{@name}`(#{property})")
20
+ end
21
+
22
+ # Creates a neo4j constraint on a property
23
+ # See http://docs.neo4j.org/chunked/stable/query-constraints.html
24
+ # @example
25
+ # label = Neo4j::Label.create(:person, session)
26
+ # label.create_constraint(:name, {type: :unique}, session)
27
+ #
28
+ def create_constraint(property, constraints)
29
+ cypher = case constraints[:type]
30
+ when :unique, :uniqueness
31
+ "CREATE CONSTRAINT ON (n:`#{name}`) ASSERT n.`#{property}` IS UNIQUE"
32
+ else
33
+ fail "Not supported constraint #{constraints.inspect} for property #{property} (expected :type => :unique)"
34
+ end
35
+ schema_query(cypher)
36
+ end
37
+
38
+ def create_uniqueness_constraint(property, options = {})
39
+ create_constraint(property, options.merge(type: :unique))
40
+ end
41
+
42
+ # Drops a neo4j constraint on a property
43
+ # See http://docs.neo4j.org/chunked/stable/query-constraints.html
44
+ # @example
45
+ # label = Neo4j::Label.create(:person, session)
46
+ # label.create_constraint(:name, {type: :unique}, session)
47
+ # label.drop_constraint(:name, {type: :unique}, session)
48
+ #
49
+ def drop_constraint(property, constraint)
50
+ cypher = case constraint[:type]
51
+ when :unique, :uniqueness
52
+ "DROP CONSTRAINT ON (n:`#{name}`) ASSERT n.`#{property}` IS UNIQUE"
53
+ else
54
+ fail "Not supported constraint #{constraint.inspect}"
55
+ end
56
+ schema_query(cypher)
57
+ end
58
+
59
+ def drop_uniqueness_constraint(property, options = {})
60
+ drop_constraint(property, options.merge(type: :unique))
61
+ end
62
+
63
+ def indexes
64
+ @session.indexes.select do |definition|
65
+ definition[:label] == @name.to_sym
66
+ end
67
+ end
68
+
69
+ def self.indexes_for(session)
70
+ session.indexes
71
+ end
72
+
73
+ def drop_indexes
74
+ indexes.each do |definition|
75
+ begin
76
+ @session.query("DROP INDEX ON :`#{definition[:label]}`(#{definition[:properties][0]})")
77
+ rescue Neo4j::Server::CypherResponse::ResponseError
78
+ # This will error on each constraint. Ignore and continue.
79
+ next
80
+ end
81
+ end
82
+ end
83
+
84
+ def self.drop_indexes_for(session)
85
+ indexes_for(session).each do |definition|
86
+ begin
87
+ session.query("DROP INDEX ON :`#{definition[:label]}`(#{definition[:properties][0]})")
88
+ rescue Neo4j::Server::CypherResponse::ResponseError
89
+ # This will error on each constraint. Ignore and continue.
90
+ next
91
+ end
92
+ end
93
+ end
94
+
95
+ def index?(property)
96
+ indexes.any? { |definition| definition[:properties] == [property.to_sym] }
97
+ end
98
+
99
+ def constraints(_options = {})
100
+ @session.constraints.select do |definition|
101
+ definition[:label] == @name.to_sym
102
+ end
103
+ end
104
+
105
+ def uniqueness_constraints(_options = {})
106
+ constraints.select do |definition|
107
+ definition[:type] == :uniqueness
108
+ end
109
+ end
110
+
111
+ def drop_uniqueness_constraints
112
+ uniqueness_constraints.each do |definition|
113
+ @session.query("DROP CONSTRAINT ON (n:`#{definition[:label]}`) ASSERT n.`#{definition[:properties][0]}` IS UNIQUE")
114
+ end
115
+ end
116
+
117
+ def self.drop_uniqueness_constraints_for(session)
118
+ session.constraints.each do |definition|
119
+ session.query("DROP CONSTRAINT ON (n:`#{definition[:label]}`) ASSERT n.`#{definition[:properties][0]}` IS UNIQUE")
120
+ end
121
+ end
122
+
123
+ def constraint?(property)
124
+ constraints.any? { |definition| definition[:properties] == [property.to_sym] }
125
+ end
126
+
127
+ def uniqueness_constraint?(property)
128
+ uniqueness_constraints.include?([property])
129
+ end
130
+
131
+ def self.wait_for_schema_changes(session)
132
+ schema_threads(session).map(&:join)
133
+ set_schema_threads(session, [])
134
+ end
135
+
136
+ private
137
+
138
+ # Store schema threads on the session so that we can easily wait for all
139
+ # threads on a session regardless of label
140
+ def schema_threads
141
+ self.class.schema_threads(@session)
142
+ end
143
+
144
+ def schema_threads=(array)
145
+ self.class.set_schema_threads(@session, array)
146
+ end
147
+
148
+ class << self
149
+ def schema_threads(session)
150
+ session.instance_variable_get('@_schema_threads') || []
151
+ end
152
+
153
+ def set_schema_threads(session, array)
154
+ session.instance_variable_set('@_schema_threads', array)
155
+ end
156
+ end
157
+
158
+ # Schema queries can run separately from other queries, but they should
159
+ # be mutually exclusive to each other or we get locking errors
160
+ # SCHEMA_QUERY_SEMAPHORE = Mutex.new
161
+
162
+ # If there is a transaction going on, this could block
163
+ # So we run in a thread and it will go through at the next opportunity
164
+ def schema_query(cypher)
165
+ # Thread.new do
166
+ # SCHEMA_QUERY_SEMAPHORE.synchronize do
167
+
168
+ @session.transaction { |tx| tx.query(cypher, {}) }
169
+ rescue Exception => e # rubocop:disable Lint/RescueException
170
+ puts 'ERROR during schema query:', e.message, e.backtrace
171
+
172
+ # end
173
+ # end.tap { |thread| schema_threads << thread }
174
+ end
175
+
176
+ def validate_index_options!(options)
177
+ return unless options[:type] && options[:type] != :exact
178
+ fail "Type #{options[:type]} is not supported"
179
+ end
180
+ end
181
+ end
182
+ end
@@ -1,20 +1,25 @@
1
1
  require 'neo4j/core/wrappable'
2
+ require 'active_support/core_ext/hash/keys'
2
3
 
3
4
  module Neo4j
4
5
  module Core
5
6
  class Node
6
7
  attr_reader :id, :labels, :properties
7
- alias_method :props, :properties
8
+ alias props properties
8
9
 
9
10
  include Wrappable
10
11
 
11
12
  # Perhaps we should deprecate this?
12
- alias_method :neo_id, :id
13
+ alias neo_id id
13
14
 
14
15
  def initialize(id, labels, properties = {})
15
16
  @id = id
16
17
  @labels = labels.map(&:to_sym) unless labels.nil?
17
- @properties = properties
18
+ @properties = properties.symbolize_keys
19
+ end
20
+
21
+ def ==(other)
22
+ other.is_a?(Node) && neo_id == other.neo_id
18
23
  end
19
24
 
20
25
  class << self
@@ -1,14 +1,22 @@
1
+ require 'neo4j/core/wrappable'
2
+ require 'active_support/core_ext/hash/keys'
3
+
1
4
  module Neo4j
2
5
  module Core
3
6
  class Relationship
4
- attr_reader :id, :type, :properties
7
+ attr_reader :id, :type, :properties, :start_node_id, :end_node_id
8
+ alias props properties
9
+ alias neo_id id
10
+ alias rel_type type
5
11
 
6
12
  include Wrappable
7
13
 
8
- def initialize(id, type, properties)
14
+ def initialize(id, type, properties, start_node_id = nil, end_node_id = nil)
9
15
  @id = id
10
16
  @type = type.to_sym unless type.nil?
11
- @properties = properties
17
+ @properties = properties.symbolize_keys
18
+ @start_node_id = start_node_id
19
+ @end_node_id = end_node_id
12
20
  end
13
21
 
14
22
  class << self
@@ -17,7 +25,7 @@ module Neo4j
17
25
  type = nil # unknown
18
26
  properties = properties
19
27
 
20
- new(id, type, properties)
28
+ new(id, type, properties, nil, nil)
21
29
  end
22
30
  end
23
31
  end
@@ -3,6 +3,6 @@ module Neo4j
3
3
  def ==(other)
4
4
  other.class == self.class && other.neo_id == neo_id
5
5
  end
6
- alias_method :eql?, :==
6
+ alias eql? ==
7
7
  end
8
8
  end
data/lib/neo4j/session.rb CHANGED
@@ -36,11 +36,6 @@ module Neo4j
36
36
  true # TODO
37
37
  end
38
38
 
39
- # @abstract
40
- def begin_tx
41
- fail 'not impl.'
42
- end
43
-
44
39
  class CypherError < StandardError
45
40
  attr_reader :error_msg, :error_status, :error_code
46
41
  def initialize(error_msg, error_code, error_status)
@@ -77,6 +72,10 @@ module Neo4j
77
72
  fail 'not implemented'
78
73
  end
79
74
 
75
+ def transaction_class
76
+ self.class.transaction_class
77
+ end
78
+
80
79
  class << self
81
80
  # Creates a new session to Neo4j.
82
81
  # This will be the default session to be used unless there is already a session created (see #current and #set_current)
@@ -1,39 +1,86 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+ require 'active_support/per_thread_registry'
3
+
1
4
  module Neo4j
2
5
  module Transaction
3
6
  extend self
4
7
 
5
- module Instance
6
- # @private
7
- def register_instance
8
- @pushed_nested = 0
9
- Neo4j::Transaction.register(self)
8
+ # Provides a simple API to manage transactions for each session in a thread-safe manner
9
+ class TransactionsRegistry
10
+ extend ActiveSupport::PerThreadRegistry
11
+
12
+ attr_accessor :transactions_by_session_id
13
+ end
14
+
15
+ class Base
16
+ attr_reader :session, :root
17
+
18
+ def initialize(session, _options = {})
19
+ @session = session
20
+
21
+ Transaction.stack_for(session) << self
22
+
23
+ @root = Transaction.stack_for(session).first
24
+ # Neo4j::Core::Label::SCHEMA_QUERY_SEMAPHORE.lock if root?
25
+
26
+ # @parent = session_transaction_stack.last
27
+ # session_transaction_stack << self
10
28
  end
11
29
 
12
- # Marks this transaction as failed, which means that it will unconditionally be rolled back when close() is called. Aliased for legacy purposes.
13
- def mark_failed
14
- @failure = true
30
+ def inspect
31
+ status_string = [:id, :failed?, :active?, :commit_url].map do |method|
32
+ "#{method}: #{send(method)}" if respond_to?(method)
33
+ end.compact.join(', ')
34
+
35
+ "<#{self.class} [#{status_string}]"
15
36
  end
16
- alias_method :failure, :mark_failed
17
37
 
18
- # If it has been marked as failed. Aliased for legacy purposes.
19
- def failed?
20
- !!@failure
38
+ # Commits or marks this transaction for rollback, depending on whether #mark_failed has been previously invoked.
39
+ def close
40
+ tx_stack = Transaction.stack_for(@session)
41
+ fail 'Tried closing when transaction stack is empty (maybe you closed too many?)' if tx_stack.empty?
42
+ fail "Closed transaction which wasn't the most recent on the stack (maybe you forgot to close one?)" if tx_stack.pop != self
43
+
44
+ @closed = true
45
+
46
+ post_close! if tx_stack.empty?
47
+ end
48
+
49
+ def delete
50
+ fail 'not implemented'
51
+ end
52
+
53
+ def commit
54
+ fail 'not implemented'
21
55
  end
22
- alias_method :failure?, :failed?
23
56
 
24
57
  def autoclosed!
25
58
  @autoclosed = true if transient_failures_autoclose?
26
59
  end
27
60
 
28
- def transient_failures_autoclose?
29
- Neo4j::Session.current.version >= '2.2.6'
61
+ def closed?
62
+ !!@closed
30
63
  end
31
64
 
32
- def autoclosed?
33
- !!@autoclosed
65
+ # Marks this transaction as failed,
66
+ # which means that it will unconditionally be rolled back
67
+ # when #close is called.
68
+ # Aliased for legacy purposes.
69
+ def mark_failed
70
+ root.mark_failed if root && root != self
71
+ @failure = true
34
72
  end
73
+ alias failure mark_failed
74
+
75
+ # If it has been marked as failed.
76
+ # Aliased for legacy purposes.
77
+ def failed?
78
+ !!@failure
79
+ end
80
+ alias failure? failed?
35
81
 
36
82
  def mark_expired
83
+ @parent.mark_expired if @parent
37
84
  @expired = true
38
85
  end
39
86
 
@@ -41,43 +88,24 @@ module Neo4j
41
88
  !!@expired
42
89
  end
43
90
 
44
- # @private
45
- def push_nested!
46
- @pushed_nested += 1
91
+ def root?
92
+ @root == self
47
93
  end
48
94
 
49
- # @private
50
- def pop_nested!
51
- @pushed_nested -= 1
52
- end
95
+ private
53
96
 
54
- # Only for the embedded neo4j !
55
- # Acquires a read lock for entity for this transaction.
56
- # See neo4j java docs.
57
- # @param [Neo4j::Node,Neo4j::Relationship] entity
58
- # @return [Java::OrgNeo4jKernelImplCoreapi::PropertyContainerLocker]
59
- def acquire_read_lock(entity)
97
+ def transient_failures_autoclose?
98
+ Gem::Version.new(@session.version) >= Gem::Version.new('2.2.6')
60
99
  end
61
100
 
62
- # Only for the embedded neo4j !
63
- # Acquires a write lock for entity for this transaction.
64
- # See neo4j java docs.
65
- # @param [Neo4j::Node,Neo4j::Relationship] entity
66
- # @return [Java::OrgNeo4jKernelImplCoreapi::PropertyContainerLocker]
67
- def acquire_write_lock(entity)
101
+ def autoclosed?
102
+ !!@autoclosed
68
103
  end
69
104
 
70
- # Commits or marks this transaction for rollback, depending on whether failure() has been previously invoked.
71
- def close
72
- pop_nested!
73
- return if @pushed_nested >= 0
74
- fail "Can't commit transaction, already committed" if @pushed_nested < -1
75
- Neo4j::Transaction.unregister(self)
76
- post_close!
105
+ def active?
106
+ !closed?
77
107
  end
78
108
 
79
- private
80
-
81
109
  def post_close!
82
110
  return if autoclosed?
83
111
  if failed?
@@ -89,56 +117,64 @@ module Neo4j
89
117
  end
90
118
 
91
119
  # @return [Neo4j::Transaction::Instance]
92
- def new(current = Session.current!)
93
- current.begin_tx
120
+ def new(session = Session.current!)
121
+ session.transaction
94
122
  end
95
123
 
96
124
  # Runs the given block in a new transaction.
97
125
  # @param [Boolean] run_in_tx if true a new transaction will not be created, instead if will simply yield to the given block
98
126
  # @@yield [Neo4j::Transaction::Instance]
99
- def run(run_in_tx = true)
127
+ def run(*args)
128
+ session, run_in_tx = session_and_run_in_tx_from_args(args)
129
+
100
130
  fail ArgumentError, 'Expected a block to run in Transaction.run' unless block_given?
101
131
 
102
132
  return yield(nil) unless run_in_tx
103
133
 
104
- tx = Neo4j::Transaction.new
134
+ tx = Neo4j::Transaction.new(session)
105
135
  yield tx
106
136
  rescue Exception => e # rubocop:disable Lint/RescueException
107
- print_exception_cause(e)
137
+ # print_exception_cause(e)
138
+
108
139
  tx.mark_failed unless tx.nil?
109
- raise
140
+ raise e
110
141
  ensure
111
142
  tx.close unless tx.nil?
112
143
  end
113
144
 
114
- # @return [Neo4j::Transaction]
115
- def current
116
- Thread.current[:neo4j_curr_tx]
117
- end
118
-
119
- # @private
120
- def print_exception_cause(exception)
121
- return if !exception.respond_to?(:cause) || !exception.cause.respond_to?(:print_stack_trace)
145
+ # To support old syntax of providing run_in_tx first
146
+ # But session first is ideal
147
+ def session_and_run_in_tx_from_args(args)
148
+ fail ArgumentError, 'Too many arguments' if args.size > 2
149
+
150
+ if args.empty?
151
+ [Session.current!, true]
152
+ else
153
+ result = args.dup
154
+ if result.size == 1
155
+ result << ([true, false].include?(args[0]) ? Session.current! : true)
156
+ end
122
157
 
123
- puts "Java Exception in a transaction, cause: #{exception.cause}"
124
- exception.cause.print_stack_trace
158
+ [true, false].include?(result[0]) ? result.reverse : result
159
+ end
125
160
  end
126
161
 
127
- # @private
128
- def unregister(tx)
129
- Thread.current[:neo4j_curr_tx] = nil if tx == Thread.current[:neo4j_curr_tx]
162
+ def current_for(session)
163
+ stack_for(session).first
130
164
  end
131
165
 
132
- # @private
133
- def register(tx)
134
- # we don't support running more then one transaction per thread
135
- fail 'Already running a transaction' if current
136
- Thread.current[:neo4j_curr_tx] = tx
166
+ def stack_for(session)
167
+ TransactionsRegistry.transactions_by_session_id ||= {}
168
+ TransactionsRegistry.transactions_by_session_id[session.object_id] ||= []
137
169
  end
138
170
 
139
- # @private
140
- def unregister_current
141
- Thread.current[:neo4j_curr_tx] = nil
171
+ private
172
+
173
+ def print_exception_cause(exception)
174
+ return if !exception.respond_to?(:cause) || !exception.cause.respond_to?(:print_stack_trace)
175
+
176
+ Core.logger.info "Java Exception in a transaction, cause: #{exception.cause}"
177
+ exception.cause.print_stack_trace
142
178
  end
143
179
  end
144
180
  end