activecypher 0.3.0 → 0.6.0

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/lib/active_cypher/base.rb +28 -8
  3. data/lib/active_cypher/bolt/driver.rb +6 -16
  4. data/lib/active_cypher/bolt/session.rb +62 -50
  5. data/lib/active_cypher/bolt/transaction.rb +95 -90
  6. data/lib/active_cypher/connection_adapters/abstract_adapter.rb +28 -0
  7. data/lib/active_cypher/connection_adapters/abstract_bolt_adapter.rb +40 -32
  8. data/lib/active_cypher/connection_adapters/memgraph_adapter.rb +44 -0
  9. data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +27 -1
  10. data/lib/active_cypher/connection_adapters/registry.rb +94 -0
  11. data/lib/active_cypher/connection_handler.rb +18 -3
  12. data/lib/active_cypher/connection_pool.rb +5 -23
  13. data/lib/active_cypher/connection_url_resolver.rb +14 -3
  14. data/lib/active_cypher/cypher_config.rb +2 -1
  15. data/lib/active_cypher/fixtures/dsl_context.rb +41 -0
  16. data/lib/active_cypher/fixtures/evaluator.rb +37 -0
  17. data/lib/active_cypher/fixtures/node_builder.rb +53 -0
  18. data/lib/active_cypher/fixtures/parser.rb +23 -0
  19. data/lib/active_cypher/fixtures/registry.rb +43 -0
  20. data/lib/active_cypher/fixtures/rel_builder.rb +96 -0
  21. data/lib/active_cypher/fixtures.rb +177 -0
  22. data/lib/active_cypher/generators/node_generator.rb +32 -3
  23. data/lib/active_cypher/generators/relationship_generator.rb +29 -2
  24. data/lib/active_cypher/generators/templates/application_graph_node.rb +1 -0
  25. data/lib/active_cypher/generators/templates/node.rb.erb +10 -6
  26. data/lib/active_cypher/generators/templates/relationship.rb.erb +7 -6
  27. data/lib/active_cypher/instrumentation.rb +186 -0
  28. data/lib/active_cypher/model/callbacks.rb +5 -13
  29. data/lib/active_cypher/model/connection_handling.rb +37 -52
  30. data/lib/active_cypher/model/connection_owner.rb +41 -33
  31. data/lib/active_cypher/model/core.rb +4 -12
  32. data/lib/active_cypher/model/countable.rb +10 -3
  33. data/lib/active_cypher/model/destruction.rb +23 -18
  34. data/lib/active_cypher/model/labelling.rb +45 -0
  35. data/lib/active_cypher/model/persistence.rb +52 -26
  36. data/lib/active_cypher/model/querying.rb +49 -25
  37. data/lib/active_cypher/railtie.rb +40 -5
  38. data/lib/active_cypher/relation.rb +10 -2
  39. data/lib/active_cypher/relationship.rb +77 -17
  40. data/lib/active_cypher/version.rb +1 -1
  41. data/lib/activecypher.rb +4 -1
  42. data/lib/cyrel/clause/set.rb +20 -10
  43. data/lib/cyrel/expression/property_access.rb +2 -0
  44. data/lib/cyrel/functions.rb +3 -1
  45. data/lib/cyrel/logging.rb +43 -0
  46. data/lib/cyrel/plus.rb +11 -0
  47. data/lib/cyrel/query.rb +7 -1
  48. data/lib/cyrel.rb +77 -18
  49. metadata +13 -2
  50. data/lib/active_cypher/connection_factory.rb +0 -130
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 56e09b9d8193d2c4c745c1973bb420ba83854cbe0b578d5d37d4e215c96c7687
4
- data.tar.gz: a53e722376ff97ae59c6fc0735c16eb15b9e2fe89fe45cda6d42c5d45237a97f
3
+ metadata.gz: 6116df314b95f0db74a73fcc1746f0e4bf9bd529d65ab04e806953103b4017b0
4
+ data.tar.gz: d85d13cbb5ad5a9b7434f3457d4dd3b2f899c47619dbdf00f129790466b19fbf
5
5
  SHA512:
6
- metadata.gz: bd26475b4774f41f37b047c558707c7fe5282718c8970f1a24148c3f6ad57186ad7b0823051e06d420d80373605f8d28721b7f934dd64e0110b05050af6af7f9
7
- data.tar.gz: 20b688e0788fe8dd84a51471ee2125d8ce8082304d4e9db724d4970b0e27da638fd4e541030982b582716d5ad1c1f86d1e88c3fbb3605437bab05c96a5e2b832
6
+ metadata.gz: 1e12458b40696e189c20cf480565446c2d2294cd2fe2b8208560dbb77ca27f38b4971f043c11531f229dee070b5950ca785906660e121b25f74d3157f89b3a38
7
+ data.tar.gz: 7dd4fbe5fc47b350789f7f55e55321238626e7bed731d56bb83d37417411f257fc53cb0b3a2223292ac64f9b971fd07d5d9a8f9fe666b8d49c872d83a9af60de
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_model'
4
+ require 'active_model/validations'
5
+ require 'active_model/callbacks'
6
+ require 'active_model/attributes'
7
+ require 'active_model/dirty'
8
+
3
9
  module ActiveCypher
4
10
  # @!parse
5
11
  # # The One True Base Class. All node models must kneel before it.
@@ -8,18 +14,25 @@ module ActiveCypher
8
14
  # @!attribute [rw] connects_to_mappings
9
15
  # @return [Hash] Because every base class needs a mapping it will never use directly.
10
16
  class_attribute :connects_to_mappings, default: {}
17
+
18
+ # Rails/ActiveModel foundations
11
19
  include Logging
20
+ include ActiveModel::Model
21
+ include ActiveModel::Validations
22
+ include ActiveModel::Callbacks
23
+ include ActiveModel::Attributes
24
+ include ActiveModel::Dirty
12
25
 
13
26
  # Let's just include every concern we can find, because why not.
14
27
  include Model::Core
28
+ include Model::Callbacks
29
+ include Model::Labelling
30
+ include Model::Querying
31
+ include Model::Abstract
15
32
  include Model::Attributes
16
33
  include Model::ConnectionOwner
17
- include Model::Callbacks
18
34
  include Model::Persistence
19
- include Model::Querying
20
- include Model::ConnectionHandling
21
35
  include Model::Destruction
22
- include Model::Abstract
23
36
  include Model::Countable
24
37
  include Model::Inspectable
25
38
 
@@ -29,15 +42,22 @@ module ActiveCypher
29
42
  # If you still don't have a connection, you get an error. It's the circle of life.
30
43
  # @return [Object] The connection instance
31
44
  def connection
32
- if (pool = connection_handler.pool(current_role, current_shard))
33
- return pool.connection
45
+ # Determine the current role (e.g., :writing, :reading)
46
+ # ActiveCypher::RuntimeRegistry.current_role defaults to :writing
47
+ # Only use db_key for pool lookup
48
+ if respond_to?(:connects_to_mappings) && connects_to_mappings.is_a?(Hash)
49
+ db_key = connects_to_mappings[:writing] # or whichever role is appropriate
50
+ if db_key && (pool = connection_handler.pool(db_key))
51
+ return pool.connection
52
+ end
34
53
  end
35
54
 
36
- # fall back to a per‑model connection created by establish_connection
37
55
  return @connection if defined?(@connection) && @connection&.active?
38
56
 
39
57
  raise ActiveCypher::ConnectionNotEstablished,
40
- "No pool for role=#{current_role.inspect} shard=#{current_shard.inspect}"
58
+ "No connection pool found for graph #{name}, db_key=#{db_key.inspect}. " \
59
+ 'Ensure `connects_to` is configured for this model or its ancestors, ' \
60
+ 'and `cypher_databases.yml` has the corresponding entries.'
41
61
  end
42
62
  end
43
63
 
@@ -12,31 +12,21 @@ module ActiveCypher
12
12
  class Driver
13
13
  DEFAULT_POOL_SIZE = ENV.fetch('BOLT_POOL_SIZE', 10).to_i
14
14
 
15
- # Map URI schemes ➞ security/verification flags
16
- # Because nothing says "enterprise" like six ways to spell 'bolt'.
17
- SCHEMES = {
18
- 'bolt' => { secure: false, verify: true },
19
- 'bolt+s' => { secure: true, verify: true },
20
- 'bolt+ssc' => { secure: true, verify: false },
21
- 'neo4j' => { secure: false, verify: true },
22
- 'neo4j+s' => { secure: true, verify: true },
23
- 'neo4j+ssc' => { secure: true, verify: false }
24
- }.freeze
25
-
26
15
  # Initializes the driver, because you can't spell "abstraction" without "action".
27
16
  #
28
17
  # @param uri [String] The Bolt URI
29
18
  # @param adapter [Object] The adapter instance
30
19
  # @param auth_token [Hash] Authentication token
31
20
  # @param pool_size [Integer] Connection pool size
32
- def initialize(uri:, adapter:, auth_token:, pool_size: DEFAULT_POOL_SIZE)
33
- @uri = URI(uri)
34
- scheme = SCHEMES.fetch(@uri.scheme) { raise ArgumentError, "Unsupported Bolt scheme: #{@uri.scheme}" }
21
+ # @param secure [Boolean] Use SSL (default: false)
22
+ # @param verify_cert [Boolean] Verify SSL certificate (default: true)
23
+ def initialize(uri:, adapter:, auth_token:, pool_size: DEFAULT_POOL_SIZE, secure: false, verify_cert: true)
24
+ @uri = URI(uri)
35
25
 
36
26
  @adapter = adapter
37
27
  @auth = auth_token
38
- @secure = scheme[:secure]
39
- @verify_cert = scheme[:verify]
28
+ @secure = secure
29
+ @verify_cert = verify_cert
40
30
 
41
31
  # Create a connection pool with the specified size
42
32
  # Because one connection is never enough for true disappointment.
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'async'
4
-
5
4
  module ActiveCypher
6
5
  module Bolt
7
6
  # A Session is the primary unit of work in the Bolt Protocol.
8
7
  # It maintains a connection to the database server and allows running queries.
9
8
  class Session
9
+ include Instrumentation
10
10
  attr_reader :connection, :database
11
11
 
12
12
  def initialize(connection, database: nil)
@@ -34,13 +34,15 @@ module ActiveCypher
34
34
  # For Memgraph, explicitly set db to nil
35
35
  db = nil if @connection.adapter.is_a?(ConnectionAdapters::MemgraphAdapter)
36
36
 
37
- if @current_transaction&.active?
38
- # If we have an active transaction, run the query within it
39
- @current_transaction.run(query, parameters)
40
- else
41
- # Auto-transaction mode: each query gets its own transaction
42
- run_transaction(mode, db: db) do |tx|
43
- tx.run(query, parameters)
37
+ instrument_query(query, parameters, context: 'Session#run', metadata: { mode: mode, db: db }) do
38
+ if @current_transaction&.active?
39
+ # If we have an active transaction, run the query within it
40
+ @current_transaction.run(query, parameters)
41
+ else
42
+ # Auto-transaction mode: each query gets its own transaction
43
+ run_transaction(mode, db: db) do |tx|
44
+ tx.run(query, parameters)
45
+ end
44
46
  end
45
47
  end
46
48
  end
@@ -55,40 +57,46 @@ module ActiveCypher
55
57
  def begin_transaction(db: nil, access_mode: :write, tx_timeout: nil, tx_metadata: nil)
56
58
  raise ConnectionError, 'Already in a transaction' if @current_transaction&.active?
57
59
 
58
- # Send BEGIN message with appropriate metadata
59
- begin_meta = {}
60
- # For Memgraph, NEVER set a database name - it doesn't support them
61
- if @connection.adapter.is_a?(ConnectionAdapters::MemgraphAdapter)
62
- # Explicitly don't set db for Memgraph
63
- begin_meta['adapter'] = 'memgraph'
64
- # Force db to nil for Memgraph
65
- nil
66
- elsif db || @database
67
- begin_meta['db'] = db || @database
68
- end
69
- begin_meta['mode'] = access_mode == :read ? 'r' : 'w'
70
- begin_meta['tx_timeout'] = tx_timeout if tx_timeout
71
- begin_meta['tx_metadata'] = tx_metadata if tx_metadata
72
- begin_meta['bookmarks'] = @bookmarks if @bookmarks&.any?
73
-
74
- begin_msg = Messaging::Begin.new(begin_meta)
75
- @connection.write_message(begin_msg)
76
-
77
- # Read response to BEGIN
78
- response = @connection.read_message
79
-
80
- case response
81
- when Messaging::Success
82
- # BEGIN succeeded, create a new transaction
83
- @current_transaction = Transaction.new(self, @bookmarks, response.metadata)
84
- when Messaging::Failure
85
- # BEGIN failed
86
- code = response.metadata['code']
87
- message = response.metadata['message']
88
- @connection.reset!
89
- raise QueryError, "Failed to begin transaction: #{code} - #{message}"
90
- else
91
- raise ProtocolError, "Unexpected response to BEGIN: #{response.class}"
60
+ metadata = { access_mode: access_mode }
61
+ metadata[:db] = db if db
62
+ metadata[:timeout] = tx_timeout if tx_timeout
63
+
64
+ instrument_transaction(:begin, nil, metadata: metadata) do
65
+ # Send BEGIN message with appropriate metadata
66
+ begin_meta = {}
67
+ # For Memgraph, NEVER set a database name - it doesn't support them
68
+ if @connection.adapter.is_a?(ConnectionAdapters::MemgraphAdapter)
69
+ # Explicitly don't set db for Memgraph
70
+ begin_meta['adapter'] = 'memgraph'
71
+ # Force db to nil for Memgraph
72
+ nil
73
+ elsif db || @database
74
+ begin_meta['db'] = db || @database
75
+ end
76
+ begin_meta['mode'] = access_mode == :read ? 'r' : 'w'
77
+ begin_meta['tx_timeout'] = tx_timeout if tx_timeout
78
+ begin_meta['tx_metadata'] = tx_metadata if tx_metadata
79
+ begin_meta['bookmarks'] = @bookmarks if @bookmarks&.any?
80
+
81
+ begin_msg = Messaging::Begin.new(begin_meta)
82
+ @connection.write_message(begin_msg)
83
+
84
+ # Read response to BEGIN
85
+ response = @connection.read_message
86
+
87
+ case response
88
+ when Messaging::Success
89
+ # BEGIN succeeded, create a new transaction
90
+ @current_transaction = Transaction.new(self, @bookmarks, response.metadata)
91
+ when Messaging::Failure
92
+ # BEGIN failed
93
+ code = response.metadata['code']
94
+ message = response.metadata['message']
95
+ @connection.reset!
96
+ raise QueryError, "Failed to begin transaction: #{code} - #{message}"
97
+ else
98
+ raise ProtocolError, "Unexpected response to BEGIN: #{response.class}"
99
+ end
92
100
  end
93
101
  end
94
102
 
@@ -181,20 +189,24 @@ module ActiveCypher
181
189
  def reset
182
190
  return if @current_transaction.nil?
183
191
 
184
- # Mark the current transaction as no longer active
185
- complete_transaction(@current_transaction)
192
+ instrument('session.reset') do
193
+ # Mark the current transaction as no longer active
194
+ complete_transaction(@current_transaction)
186
195
 
187
- # Reset the connection
188
- @connection.reset!
196
+ # Reset the connection
197
+ @connection.reset!
198
+ end
189
199
  end
190
200
 
191
201
  # Close the session and any active transaction.
192
202
  def close
193
- # If there's an active transaction, try to roll it back
194
- @current_transaction&.rollback if @current_transaction&.active?
203
+ instrument('session.close') do
204
+ # If there's an active transaction, try to roll it back
205
+ @current_transaction&.rollback if @current_transaction&.active?
195
206
 
196
- # Mark current transaction as complete
197
- complete_transaction(@current_transaction) if @current_transaction
207
+ # Mark current transaction as complete
208
+ complete_transaction(@current_transaction) if @current_transaction
209
+ end
198
210
  end
199
211
  end
200
212
  end
@@ -4,7 +4,8 @@ module ActiveCypher
4
4
  module Bolt
5
5
  # Manages transaction state (BEGIN/COMMIT/ROLLBACK) and runs queries within a transaction.
6
6
  class Transaction
7
- attr_reader :bookmarks, :metadata
7
+ include Instrumentation
8
+ attr_reader :bookmarks, :metadata, :connection
8
9
 
9
10
  # Initializes a new Transaction instance.
10
11
  #
@@ -30,66 +31,68 @@ module ActiveCypher
30
31
  # Ensure query is a string
31
32
  query_str = query.is_a?(String) ? query : query.to_s
32
33
 
33
- # Send RUN message
34
- run_metadata = {}
35
- run_msg = Messaging::Run.new(query_str, parameters, run_metadata)
36
- @connection.write_message(run_msg)
37
-
38
- # Read response to RUN
39
- response = @connection.read_message
40
- qid = -1
41
- fields = []
42
-
43
- case response
44
- when Messaging::Success
45
- # RUN succeeded, extract metadata
46
-
47
- qid = response.metadata['qid'] if response.metadata.key?('qid')
48
- fields = response.metadata['fields'] if response.metadata.key?('fields')
49
-
50
- # Send PULL to get all records (-1 = all)
51
- pull_metadata = { 'n' => -1 }
52
- pull_metadata['qid'] = qid if qid != -1
53
- pull_msg = Messaging::Pull.new(pull_metadata)
54
- @connection.write_message(pull_msg)
55
-
56
- # Process PULL response(s)
57
- records = []
58
- summary_metadata = {}
59
-
60
- # Read messages until we get a SUCCESS (or FAILURE)
61
- loop do
62
- msg = @connection.read_message
63
- case msg
64
- when Messaging::Record
65
- # Store record with raw values - processing will happen in the adapter
66
- records << msg.values
67
- when Messaging::Success
68
- # Final SUCCESS with summary metadata
69
- summary_metadata = msg.metadata
70
- break # Done processing results
71
- when Messaging::Failure
72
- connection.reset!
73
- # PULL failed - transaction is now failed
74
- @state = :failed
75
- code = msg.metadata['code']
76
- message = msg.metadata['message']
77
- raise QueryError, "Query execution failed: #{code} - #{message}"
78
- else
79
- raise ProtocolError, "Unexpected message type: #{msg.class}"
34
+ instrument_query(query_str, parameters, context: 'Transaction#run', metadata: { transaction_id: object_id }) do
35
+ # Send RUN message
36
+ run_metadata = {}
37
+ run_msg = Messaging::Run.new(query_str, parameters, run_metadata)
38
+ connection.write_message(run_msg)
39
+
40
+ # Read response to RUN
41
+ response = connection.read_message
42
+ qid = -1
43
+ fields = []
44
+
45
+ case response
46
+ when Messaging::Success
47
+ # RUN succeeded, extract metadata
48
+
49
+ qid = response.metadata['qid'] if response.metadata.key?('qid')
50
+ fields = response.metadata['fields'] if response.metadata.key?('fields')
51
+
52
+ # Send PULL to get all records (-1 = all)
53
+ pull_metadata = { 'n' => -1 }
54
+ pull_metadata['qid'] = qid if qid != -1
55
+ pull_msg = Messaging::Pull.new(pull_metadata)
56
+ connection.write_message(pull_msg)
57
+
58
+ # Process PULL response(s)
59
+ records = []
60
+ summary_metadata = {}
61
+
62
+ # Read messages until we get a SUCCESS (or FAILURE)
63
+ loop do
64
+ msg = connection.read_message
65
+ case msg
66
+ when Messaging::Record
67
+ # Store record with raw values - processing will happen in the adapter
68
+ records << msg.values
69
+ when Messaging::Success
70
+ # Final SUCCESS with summary metadata
71
+ summary_metadata = msg.metadata
72
+ break # Done processing results
73
+ when Messaging::Failure
74
+ connection.reset!
75
+ # PULL failed - transaction is now failed
76
+ @state = :failed
77
+ code = msg.metadata['code']
78
+ message = msg.metadata['message']
79
+ raise QueryError, "Query execution failed: #{code} - #{message}"
80
+ else
81
+ raise ProtocolError, "Unexpected message type: #{msg.class}"
82
+ end
80
83
  end
81
- end
82
84
 
83
- # Create and return Result object
84
- Result.new(fields, records, summary_metadata, qid)
85
- when Messaging::Failure
86
- # RUN failed - transaction is now failed
87
- @state = :failed
88
- code = response.metadata['code']
89
- message = response.metadata['message']
90
- raise QueryError, "Query execution failed: #{code} - #{message}"
91
- else
92
- raise ProtocolError, "Unexpected response to RUN: #{response.class}"
85
+ # Create and return Result object
86
+ Result.new(fields, records, summary_metadata, qid)
87
+ when Messaging::Failure
88
+ # RUN failed - transaction is now failed
89
+ @state = :failed
90
+ code = response.metadata['code']
91
+ message = response.metadata['message']
92
+ raise QueryError, "Query execution failed: #{code} - #{message}"
93
+ else
94
+ raise ProtocolError, "Unexpected response to RUN: #{response.class}"
95
+ end
93
96
  end
94
97
  rescue ConnectionError
95
98
  @state = :failed
@@ -102,42 +105,44 @@ module ActiveCypher
102
105
  def commit
103
106
  raise ConnectionError, "Cannot commit a #{@state} transaction" unless @state == :active
104
107
 
105
- # Send COMMIT message
106
- commit_msg = Messaging::Commit.new
107
- @connection.write_message(commit_msg)
108
+ instrument_transaction(:commit, object_id) do
109
+ # Send COMMIT message
110
+ commit_msg = Messaging::Commit.new
111
+ connection.write_message(commit_msg)
108
112
 
109
- # Read response to COMMIT
110
- response = @connection.read_message
113
+ # Read response to COMMIT
114
+ response = connection.read_message
111
115
 
112
- case response
113
- when Messaging::Success
114
- # COMMIT succeeded
116
+ case response
117
+ when Messaging::Success
118
+ # COMMIT succeeded
115
119
 
116
- @state = :committed
120
+ @state = :committed
117
121
 
118
- # Extract bookmarks if any
119
- new_bookmarks = []
120
- if response.metadata.key?('bookmark')
121
- new_bookmarks = [response.metadata['bookmark']]
122
- @bookmarks = new_bookmarks
123
- end
122
+ # Extract bookmarks if any
123
+ new_bookmarks = []
124
+ if response.metadata.key?('bookmark')
125
+ new_bookmarks = [response.metadata['bookmark']]
126
+ @bookmarks = new_bookmarks
127
+ end
124
128
 
125
- # Mark transaction as completed in the session
126
- @session.complete_transaction(self, new_bookmarks)
129
+ # Mark transaction as completed in the session
130
+ @session.complete_transaction(self, new_bookmarks)
127
131
 
128
- new_bookmarks
129
- when Messaging::Failure
130
- # COMMIT failed
131
- @state = :failed
132
- code = response.metadata['code']
133
- message = response.metadata['message']
132
+ new_bookmarks
133
+ when Messaging::Failure
134
+ # COMMIT failed
135
+ @state = :failed
136
+ code = response.metadata['code']
137
+ message = response.metadata['message']
134
138
 
135
- # Mark transaction as completed in the session
136
- @session.complete_transaction(self)
139
+ # Mark transaction as completed in the session
140
+ @session.complete_transaction(self)
137
141
 
138
- raise QueryError, "Failed to commit transaction: #{code} - #{message}"
139
- else
140
- raise ProtocolError, "Unexpected response to COMMIT: #{response.class}"
142
+ raise QueryError, "Failed to commit transaction: #{code} - #{message}"
143
+ else
144
+ raise ProtocolError, "Unexpected response to COMMIT: #{response.class}"
145
+ end
141
146
  end
142
147
  rescue ConnectionError
143
148
  @state = :failed
@@ -155,13 +160,13 @@ module ActiveCypher
155
160
  # If already committed or rolled back, do nothing
156
161
  return if @state == :committed || @state == :rolled_back
157
162
 
158
- begin
163
+ instrument_transaction(:rollback, object_id) do
159
164
  # Send ROLLBACK message
160
165
  rollback_msg = Messaging::Rollback.new
161
- @connection.write_message(rollback_msg)
166
+ connection.write_message(rollback_msg)
162
167
 
163
168
  # Read response to ROLLBACK
164
- response = @connection.read_message
169
+ response = connection.read_message
165
170
 
166
171
  case response
167
172
  when Messaging::Success
@@ -49,6 +49,34 @@ module ActiveCypher
49
49
  end
50
50
  end
51
51
 
52
+ # Get current adapter type for ID handling
53
+ # Helper for generating ID-related Cypher functions that are database-specific
54
+ module CypherFunction
55
+ # Generate ID equality clause with the ID value embedded in the query for Memgraph
56
+ def self.id_equals(var, id_value, adapter)
57
+ func = id_function(adapter)
58
+ "#{func}(#{var}) = #{id_value}"
59
+ end
60
+
61
+ # Generate ID equality clause using a parameterized ID value for Neo4j
62
+ def self.id_equals_param(var, param_name, adapter)
63
+ func = id_function(adapter)
64
+ "#{func}(#{var}) = $#{param_name}"
65
+ end
66
+
67
+ # Generate a node variable with ID predicate
68
+ def self.node_with_id(node_var, id_value, adapter)
69
+ func = id_function(adapter)
70
+ "#{func}(#{node_var}) = #{id_value}"
71
+ end
72
+
73
+ # Return ID expression
74
+ def self.return_id(var, as_name, adapter)
75
+ func = id_function(adapter)
76
+ "#{func}(#{var}) AS #{as_name}"
77
+ end
78
+ end
79
+
52
80
  # Turns rows into symbols, because Rubyists fear strings.
53
81
  # @param rows [Array<Hash>] The rows to process
54
82
  # @return [Array<Hash>] The processed rows
@@ -8,6 +8,7 @@ module ActiveCypher
8
8
  # Concrete subclasses must provide protocol_handler_class, validate_connection, and execute_cypher.
9
9
  # It's like ActiveRecord::ConnectionAdapter, but for weirdos like me who use graph databases.
10
10
  class AbstractBoltAdapter < AbstractAdapter
11
+ include Instrumentation
11
12
  attr_reader :connection
12
13
 
13
14
  # Establish a connection if not already active.
@@ -15,29 +16,31 @@ module ActiveCypher
15
16
  def connect
16
17
  return true if active?
17
18
 
18
- # Determine host and port from config
19
- host, port = if config[:uri]
20
- # Legacy URI format
21
- uri = URI(config[:uri])
22
- [uri.host, uri.port || 7687]
23
- else
24
- # New URL format via ConnectionUrlResolver
25
- [config[:host] || 'localhost', config[:port] || 7687]
26
- end
27
-
28
- # Prepare auth token
29
- auth = if config[:username]
30
- { scheme: 'basic', principal: config[:username], credentials: config[:password] }
31
- else
32
- { scheme: 'none' }
33
- end
34
-
35
- @connection = Bolt::Connection.new(
36
- host, port, self,
37
- auth_token: auth, timeout_seconds: config.fetch(:timeout, 15)
38
- )
39
- @connection.connect
40
- validate_connection
19
+ instrument_connection(:connect, config) do
20
+ # Determine host and port from config
21
+ host, port = if config[:uri]
22
+ # Legacy URI format
23
+ uri = URI(config[:uri])
24
+ [uri.host, uri.port || 7687]
25
+ else
26
+ # New URL format via ConnectionUrlResolver
27
+ [config[:host] || 'localhost', config[:port] || 7687]
28
+ end
29
+
30
+ # Prepare auth token
31
+ auth = if config[:username]
32
+ { scheme: 'basic', principal: config[:username], credentials: config[:password] }
33
+ else
34
+ { scheme: 'none' }
35
+ end
36
+
37
+ @connection = Bolt::Connection.new(
38
+ host, port, self,
39
+ auth_token: auth, timeout_seconds: config.fetch(:timeout, 15)
40
+ )
41
+ @connection.connect
42
+ validate_connection
43
+ end
41
44
  end
42
45
 
43
46
  # Connection health check. If this returns false, you're probably in trouble.
@@ -45,9 +48,11 @@ module ActiveCypher
45
48
 
46
49
  # Clean disconnection. Resets the internal state.
47
50
  def disconnect
48
- @connection&.close
49
- @connection = nil
50
- true
51
+ instrument_connection(:disconnect) do
52
+ @connection&.close
53
+ @connection = nil
54
+ true
55
+ end
51
56
  end
52
57
 
53
58
  # Runs a Cypher query via Bolt session.
@@ -55,11 +60,14 @@ module ActiveCypher
55
60
  def run(cypher, params = {}, context: 'Query', db: nil, access_mode: :write)
56
61
  connect
57
62
  logger.debug { "[#{context}] #{cypher} #{params.inspect}" }
58
- session = Bolt::Session.new(connection, default_db: db)
59
- result = session.run(cypher, prepare_params(params), access_mode:)
60
- rows = result.respond_to?(:to_a) ? result.to_a : result
61
- session.close
62
- rows
63
+
64
+ instrument_query(cypher, params, context: context, metadata: { db: db, access_mode: access_mode }) do
65
+ session = Bolt::Session.new(connection, database: db)
66
+ result = session.run(cypher, prepare_params(params), mode: access_mode)
67
+ rows = result.respond_to?(:to_a) ? result.to_a : result
68
+ session.close
69
+ rows
70
+ end
63
71
  end
64
72
 
65
73
  # Convert access mode to database-specific format
@@ -84,7 +92,7 @@ module ActiveCypher
84
92
  # you will be publicly shamed by a NotImplementedError.
85
93
  def protocol_handler_class = raise(NotImplementedError)
86
94
  def validate_connection = raise(NotImplementedError)
87
- def execute_cypher(*) = raise(NotImplementedError)
95
+ def execute_cypher(*) = raise(NotImplementedError, "#{self.class} must implement #execute_cypher")
88
96
 
89
97
  private
90
98