activecypher 0.0.0 → 0.2.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/lib/active_cypher/associations/collection_proxy.rb +144 -0
  3. data/lib/active_cypher/associations.rb +537 -0
  4. data/lib/active_cypher/base.rb +47 -0
  5. data/lib/active_cypher/bolt/connection.rb +525 -0
  6. data/lib/active_cypher/bolt/driver.rb +144 -0
  7. data/lib/active_cypher/bolt/handlers.rb +10 -0
  8. data/lib/active_cypher/bolt/message_reader.rb +100 -0
  9. data/lib/active_cypher/bolt/message_writer.rb +53 -0
  10. data/lib/active_cypher/bolt/messaging.rb +307 -0
  11. data/lib/active_cypher/bolt/packstream.rb +319 -0
  12. data/lib/active_cypher/bolt/result.rb +82 -0
  13. data/lib/active_cypher/bolt/session.rb +201 -0
  14. data/lib/active_cypher/bolt/transaction.rb +211 -0
  15. data/lib/active_cypher/bolt/version_encoding.rb +41 -0
  16. data/lib/active_cypher/bolt.rb +7 -0
  17. data/lib/active_cypher/connection_adapters/abstract_adapter.rb +75 -0
  18. data/lib/active_cypher/connection_adapters/abstract_bolt_adapter.rb +178 -0
  19. data/lib/active_cypher/connection_adapters/memgraph_adapter.rb +44 -0
  20. data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +58 -0
  21. data/lib/active_cypher/connection_factory.rb +130 -0
  22. data/lib/active_cypher/connection_handler.rb +9 -0
  23. data/lib/active_cypher/connection_pool.rb +123 -0
  24. data/lib/active_cypher/connection_url_resolver.rb +137 -0
  25. data/lib/active_cypher/cypher_config.rb +50 -0
  26. data/lib/active_cypher/generators/install_generator.rb +23 -0
  27. data/lib/active_cypher/generators/node_generator.rb +32 -0
  28. data/lib/active_cypher/generators/relationship_generator.rb +33 -0
  29. data/lib/active_cypher/generators/templates/application_graph_node.rb +5 -0
  30. data/lib/active_cypher/generators/templates/application_graph_relationship.rb +5 -0
  31. data/lib/active_cypher/generators/templates/cypher_databases.yml +28 -0
  32. data/lib/active_cypher/generators/templates/node.rb.erb +10 -0
  33. data/lib/active_cypher/generators/templates/relationship.rb.erb +11 -0
  34. data/lib/active_cypher/logging.rb +44 -0
  35. data/lib/active_cypher/model/abstract.rb +87 -0
  36. data/lib/active_cypher/model/attributes.rb +24 -0
  37. data/lib/active_cypher/model/callbacks.rb +44 -0
  38. data/lib/active_cypher/model/connection_handling.rb +76 -0
  39. data/lib/active_cypher/model/connection_owner.rb +50 -0
  40. data/lib/active_cypher/model/core.rb +45 -0
  41. data/lib/active_cypher/model/countable.rb +30 -0
  42. data/lib/active_cypher/model/destruction.rb +49 -0
  43. data/lib/active_cypher/model/inspectable.rb +28 -0
  44. data/lib/active_cypher/model/persistence.rb +182 -0
  45. data/lib/active_cypher/model/querying.rb +67 -0
  46. data/lib/active_cypher/railtie.rb +34 -0
  47. data/lib/active_cypher/relation.rb +190 -0
  48. data/lib/active_cypher/relationship.rb +233 -0
  49. data/lib/active_cypher/runtime_registry.rb +8 -0
  50. data/lib/active_cypher/scoping.rb +97 -0
  51. data/lib/active_cypher/utils/logger.rb +100 -0
  52. data/lib/active_cypher/version.rb +5 -0
  53. data/lib/activecypher.rb +108 -0
  54. data/lib/cyrel/call_procedure.rb +29 -0
  55. data/lib/cyrel/clause/call.rb +46 -0
  56. data/lib/cyrel/clause/call_subquery.rb +40 -0
  57. data/lib/cyrel/clause/create.rb +33 -0
  58. data/lib/cyrel/clause/delete.rb +41 -0
  59. data/lib/cyrel/clause/limit.rb +33 -0
  60. data/lib/cyrel/clause/match.rb +40 -0
  61. data/lib/cyrel/clause/merge.rb +34 -0
  62. data/lib/cyrel/clause/order_by.rb +78 -0
  63. data/lib/cyrel/clause/remove.rb +75 -0
  64. data/lib/cyrel/clause/return.rb +90 -0
  65. data/lib/cyrel/clause/set.rb +97 -0
  66. data/lib/cyrel/clause/skip.rb +34 -0
  67. data/lib/cyrel/clause/where.rb +42 -0
  68. data/lib/cyrel/clause/with.rb +94 -0
  69. data/lib/cyrel/clause.rb +25 -0
  70. data/lib/cyrel/direction.rb +18 -0
  71. data/lib/cyrel/expression/alias.rb +27 -0
  72. data/lib/cyrel/expression/base.rb +101 -0
  73. data/lib/cyrel/expression/case.rb +45 -0
  74. data/lib/cyrel/expression/comparison.rb +60 -0
  75. data/lib/cyrel/expression/exists.rb +42 -0
  76. data/lib/cyrel/expression/function_call.rb +57 -0
  77. data/lib/cyrel/expression/literal.rb +33 -0
  78. data/lib/cyrel/expression/logical.rb +38 -0
  79. data/lib/cyrel/expression/operator.rb +27 -0
  80. data/lib/cyrel/expression/pattern_comprehension.rb +44 -0
  81. data/lib/cyrel/expression/property_access.rb +25 -0
  82. data/lib/cyrel/expression.rb +56 -0
  83. data/lib/cyrel/functions.rb +116 -0
  84. data/lib/cyrel/node.rb +397 -0
  85. data/lib/cyrel/parameterizable.rb +20 -0
  86. data/lib/cyrel/pattern/node.rb +66 -0
  87. data/lib/cyrel/pattern/path.rb +41 -0
  88. data/lib/cyrel/pattern/relationship.rb +74 -0
  89. data/lib/cyrel/pattern.rb +8 -0
  90. data/lib/cyrel/query.rb +497 -0
  91. data/lib/cyrel/return_only.rb +26 -0
  92. data/lib/cyrel/types/hash_type.rb +22 -0
  93. data/lib/cyrel/types/symbol_type.rb +13 -0
  94. data/lib/cyrel.rb +72 -0
  95. data/lib/tasks/active_cypher_tasks.rake +6 -0
  96. data/sig/activecypher.rbs +4 -0
  97. metadata +173 -10
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveCypher
4
+ module Bolt
5
+ # Manages transaction state (BEGIN/COMMIT/ROLLBACK) and runs queries within a transaction.
6
+ class Transaction
7
+ attr_reader :bookmarks, :metadata
8
+
9
+ # Initializes a new Transaction instance.
10
+ #
11
+ # @param session [Session] The session that owns this transaction.
12
+ # @param initial_bookmarks [Array<String>] Initial bookmarks for causal consistency.
13
+ # @param metadata [Hash] Metadata from the BEGIN success response.
14
+ def initialize(session, initial_bookmarks, metadata = {})
15
+ @session = session
16
+ @connection = session.connection
17
+ @bookmarks = initial_bookmarks || []
18
+ @metadata = metadata
19
+ @state = :active
20
+ end
21
+
22
+ # Runs a Cypher query within this transaction.
23
+ #
24
+ # @param query [String] The Cypher query to execute.
25
+ # @param parameters [Hash] Parameters for the query.
26
+ # @return [Result] The result of the query execution.
27
+ def run(query, parameters = {})
28
+ raise ConnectionError, "Cannot run query on a #{@state} transaction" unless @state == :active
29
+
30
+ # Ensure query is a string
31
+ query_str = query.is_a?(String) ? query : query.to_s
32
+
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}"
80
+ end
81
+ end
82
+
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}"
93
+ end
94
+ rescue ConnectionError
95
+ @state = :failed
96
+ raise
97
+ end
98
+
99
+ # Commits the transaction.
100
+ #
101
+ # @return [Array<String>] Any new bookmarks.
102
+ def commit
103
+ raise ConnectionError, "Cannot commit a #{@state} transaction" unless @state == :active
104
+
105
+ # Send COMMIT message
106
+ commit_msg = Messaging::Commit.new
107
+ @connection.write_message(commit_msg)
108
+
109
+ # Read response to COMMIT
110
+ response = @connection.read_message
111
+
112
+ case response
113
+ when Messaging::Success
114
+ # COMMIT succeeded
115
+
116
+ @state = :committed
117
+
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
124
+
125
+ # Mark transaction as completed in the session
126
+ @session.complete_transaction(self, new_bookmarks)
127
+
128
+ new_bookmarks
129
+ when Messaging::Failure
130
+ # COMMIT failed
131
+ @state = :failed
132
+ code = response.metadata['code']
133
+ message = response.metadata['message']
134
+
135
+ # Mark transaction as completed in the session
136
+ @session.complete_transaction(self)
137
+
138
+ raise QueryError, "Failed to commit transaction: #{code} - #{message}"
139
+ else
140
+ raise ProtocolError, "Unexpected response to COMMIT: #{response.class}"
141
+ end
142
+ rescue ConnectionError
143
+ @state = :failed
144
+ # Mark transaction as completed in the session
145
+ begin
146
+ @session.complete_transaction(self)
147
+ rescue StandardError
148
+ nil
149
+ end
150
+ raise
151
+ end
152
+
153
+ # Rolls back the transaction.
154
+ def rollback
155
+ # If already committed or rolled back, do nothing
156
+ return if @state == :committed || @state == :rolled_back
157
+
158
+ begin
159
+ # Send ROLLBACK message
160
+ rollback_msg = Messaging::Rollback.new
161
+ @connection.write_message(rollback_msg)
162
+
163
+ # Read response to ROLLBACK
164
+ response = @connection.read_message
165
+
166
+ case response
167
+ when Messaging::Success
168
+ # ROLLBACK succeeded
169
+
170
+ when Messaging::Failure
171
+ # ROLLBACK failed - unusual but possible if connection is in a bad state
172
+ response.metadata['code']
173
+ response.metadata['message']
174
+
175
+ # We don't raise here to ensure the rollback doesn't throw exceptions
176
+ end
177
+ rescue StandardError
178
+ # We catch all exceptions during rollback to ensure it doesn't throw
179
+ ensure
180
+ # Always mark as rolled back and complete the transaction
181
+ @state = :rolled_back
182
+ begin
183
+ @session.complete_transaction(self)
184
+ rescue StandardError
185
+ nil
186
+ end
187
+ end
188
+ end
189
+
190
+ # Checks if the transaction is active.
191
+ def active?
192
+ @state == :active
193
+ end
194
+
195
+ # Checks if the transaction is committed.
196
+ def committed?
197
+ @state == :committed
198
+ end
199
+
200
+ # Checks if the transaction is rolled back.
201
+ def rolled_back?
202
+ @state == :rolled_back
203
+ end
204
+
205
+ # Checks if the transaction is in a failed state.
206
+ def failed?
207
+ @state == :failed
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveCypher
4
+ module Bolt
5
+ module VersionEncoding
6
+ private
7
+
8
+ # ----------------------------
9
+ # Encode: [0,0,minor,major]
10
+ # ----------------------------
11
+ # Accepts Float (5.8), String ('5.8'), Integer (5) or [major,minor]
12
+ def encode_version(ver)
13
+ major, minor =
14
+ case ver
15
+ when Float then [ver.to_i, (ver * 10).round % 10]
16
+ when String then ver.split('.').map(&:to_i)
17
+ when Integer then [ver, 0]
18
+ when Array then ver
19
+ else
20
+ raise ArgumentError, "Unsupported version #{ver.inspect}"
21
+ end
22
+
23
+ [0, 0, minor, major].pack('C4')
24
+ end
25
+
26
+ # ----------------------------
27
+ # Decode: extract minor / major
28
+ # ----------------------------
29
+ def decode_version(bytes)
30
+ raise ArgumentError, 'need 4 bytes' unless bytes.bytesize == 4
31
+
32
+ minor = bytes.getbyte(2)
33
+ major = bytes.getbyte(3)
34
+
35
+ return 0.0 if major.zero? && minor.zero?
36
+
37
+ "#{major}.#{minor}".to_f # or return [major, minor]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveCypher
4
+ # Namespace for the Bolt Protocol Driver implementation.
5
+ module Bolt
6
+ end
7
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'date'
5
+ require 'active_support/core_ext/hash/indifferent_access'
6
+
7
+ module ActiveCypher
8
+ module ConnectionAdapters
9
+ # Minimal contract every graph adapter must fulfil.
10
+ # @note Because every project needs an abstract class to remind you that nothing is ever truly implemented.
11
+ class AbstractAdapter
12
+ attr_reader :config
13
+
14
+ # Initializes the adapter, because you can't spell "configuration" without "con."
15
+ # @param config [Hash] The configuration hash for the adapter
16
+ def initialize(config) = (@config = config)
17
+
18
+ # ---- lifecycle ---------------------------------------------------------
19
+ # The lifecycle methods. Spoiler: most of them do nothing.
20
+ def connect = raise(AdapterNotFoundError)
21
+ def disconnect = true
22
+ def active? = false
23
+ def reconnect = disconnect && connect
24
+
25
+ # ---- Cypher ------------------------------------------------------------
26
+ # Executes a Cypher query, or at least raises an error about it.
27
+ # @raise [NotImplementedError] Always, unless implemented by subclass.
28
+ def execute_cypher(*)
29
+ raise NotImplementedError, "#{self.class} must implement #execute_cypher"
30
+ end
31
+
32
+ # ---- transactions (optional) ------------------------------------------
33
+ # Transaction methods: for when you want to pretend you have ACID.
34
+ def begin_transaction = nil
35
+ def commit_transaction(_) = true
36
+ def rollback_transaction(_) = true
37
+
38
+ # ---- helpers -----------------------------------------------------------
39
+ # Prepares parameters for Cypher, because the database can't read your mind. Yet.
40
+ # @param raw [Object] The raw parameter value
41
+ # @return [Object] The prepared parameter
42
+ def prepare_params(raw)
43
+ case raw
44
+ when Hash then raw.transform_keys(&:to_s).transform_values { |v| prepare_params(v) }
45
+ when Array then raw.each_with_index.to_h { |v, i| ["p#{i + 1}", prepare_params(v)] }
46
+ when Time, Date, DateTime then raw.iso8601
47
+ when Symbol then raw.to_s
48
+ else raw # String/Integer/Float/Boolean/NilClass
49
+ end
50
+ end
51
+
52
+ # Turns rows into symbols, because Rubyists fear strings.
53
+ # @param rows [Array<Hash>] The rows to process
54
+ # @return [Array<Hash>] The processed rows
55
+ def process_records(rows) = rows.map { |r| deep_symbolize(r) }
56
+
57
+ private
58
+
59
+ # Recursively turns everything into symbols, because that's what all the cool kids do.
60
+ # @param obj [Object] The object to symbolize
61
+ # @return [Object] The symbolized object
62
+ def deep_symbolize(obj)
63
+ case obj
64
+ when Hash then obj.transform_keys(&:to_sym).transform_values { |v| deep_symbolize(v) }
65
+ when Array then obj.map { |v| deep_symbolize(v) }
66
+ else obj
67
+ end
68
+ end
69
+
70
+ # Returns the logger, or creates a new one if Rails isn't watching.
71
+ # @return [Logger] The logger instance
72
+ def logger = defined?(Rails) ? Rails.logger : Logger.new($stdout)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module ActiveCypher
6
+ module ConnectionAdapters
7
+ # Abstract adapter for Bolt-based graph databases.
8
+ # Concrete subclasses must provide protocol_handler_class, validate_connection, and execute_cypher.
9
+ # It's like ActiveRecord::ConnectionAdapter, but for weirdos like me who use graph databases.
10
+ class AbstractBoltAdapter < AbstractAdapter
11
+ attr_reader :connection
12
+
13
+ # Establish a connection if not already active.
14
+ # This includes auth token prep, URI parsing, and quiet suffering.
15
+ def connect
16
+ return true if active?
17
+
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
41
+ end
42
+
43
+ # Connection health check. If this returns false, you're probably in trouble.
44
+ def active? = @connection&.connected?
45
+
46
+ # Clean disconnection. Resets the internal state.
47
+ def disconnect
48
+ @connection&.close
49
+ @connection = nil
50
+ true
51
+ end
52
+
53
+ # Runs a Cypher query via Bolt session.
54
+ # Automatically handles connect, logs query, cleans up session. Very adult.
55
+ def run(cypher, params = {}, context: 'Query', db: nil, access_mode: :write)
56
+ connect
57
+ 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
+ end
64
+
65
+ # Convert access mode to database-specific format
66
+ def convert_access_mode(mode)
67
+ mode.to_s # Default implementation
68
+ end
69
+
70
+ # Prepare transaction metadata with database-specific attributes
71
+ def prepare_tx_metadata(metadata, _db, _access_mode)
72
+ metadata # Default implementation
73
+ end
74
+
75
+ # Create a protocol handler for the connection
76
+ def create_protocol_handler(connection)
77
+ protocol_handler_class.new(connection)
78
+ # Return handler for connection to store
79
+ end
80
+
81
+ protected
82
+
83
+ # These must be defined by subclasses. If you don't override them,
84
+ # you will be publicly shamed by a NotImplementedError.
85
+ def protocol_handler_class = raise(NotImplementedError)
86
+ def validate_connection = raise(NotImplementedError)
87
+ def execute_cypher(*) = raise(NotImplementedError)
88
+
89
+ private
90
+
91
+ # ------------------------------------------------------------------
92
+ # DANGER‑ZONE ‑‑ full‑graph eraser
93
+ #
94
+ # 🔥 Use *only* when you're absolutely certain, or when you need
95
+ # a dramatic way to prove you're "senior‑material." (Nothing
96
+ # says "promotion potential" like nuking the staging graph in
97
+ # front of the team, right?)
98
+ #
99
+ # Call it with:
100
+ # adapter.send(:wipe_database, confirm: "yes, really")
101
+ #
102
+ # Options:
103
+ # :confirm => string # mandatory safety latch
104
+ # :batch => integer # optional batch size for huge graphs
105
+ #
106
+ # Returns true on success.
107
+ # ------------------------------------------------------------------
108
+ def wipe_database(confirm:, batch: nil)
109
+ raise 'Refusing to wipe without explicit confirmation' unless confirm == 'yes, really'
110
+
111
+ if batch
112
+ # Manual batch wipe in case of ginormous graphs.
113
+ loop do
114
+ deleted = execute_cypher(<<~CYPHER, {}, 'Batch‑Delete')
115
+ CALL {
116
+ MATCH ()-[r]-()
117
+ WITH r LIMIT #{batch}
118
+ DELETE r
119
+ RETURN count(r) AS rels
120
+ }
121
+ CALL {
122
+ MATCH (n)
123
+ WITH n LIMIT #{batch}
124
+ DELETE n
125
+ RETURN count(n) AS nodes
126
+ }
127
+ RETURN rels + nodes AS total
128
+ CYPHER
129
+ break if deleted.first[:total].zero?
130
+ end
131
+ else
132
+ # Regular wipe: burn it all.
133
+ execute_cypher('MATCH (n) DETACH DELETE n', {}, 'WipeDB')
134
+ end
135
+ true
136
+ end
137
+
138
+ # ------------------------------------------------------------------
139
+ # Converts a Bolt‑encoded Node into a simple Ruby hash.
140
+ # Because we just want the props, not a dissertation on labels.
141
+ # ------------------------------------------------------------------
142
+ def process_node(bolt_array)
143
+ return bolt_array unless bolt_array.is_a?(Array) && bolt_array.first == 78
144
+
145
+ _id, _labels, props = bolt_array[1] # we only care about the props
146
+ props
147
+ end
148
+ end
149
+
150
+ # ------------------------------------------------------------------
151
+ # AbstractProtocolHandler
152
+ # Handles low‑level connection protocol things like version parsing
153
+ # and resetting the session state. It's like a janitor for Bolt.
154
+ # ------------------------------------------------------------------
155
+ class AbstractProtocolHandler
156
+ attr_reader :connection, :server_version
157
+
158
+ def initialize(connection)
159
+ @connection = connection
160
+ @server_version = extract_version(connection.server_agent.to_s)
161
+ end
162
+
163
+ # Extract the server version string from the agent header.
164
+ # Subclass this if you want to pretend you're compatible.
165
+ def extract_version(_agent) = 'unknown'
166
+
167
+ # Sends a Bolt RESET to clear the server's mental state.
168
+ # Great for when you've made a mess and don't want to talk about it.
169
+ def reset!
170
+ connection.write_message(Bolt::Messaging::Reset.new)
171
+ msg = connection.read_message
172
+ msg.is_a?(Bolt::Messaging::Success)
173
+ rescue StandardError
174
+ false
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveCypher
4
+ module ConnectionAdapters
5
+ class MemgraphAdapter < AbstractBoltAdapter
6
+ # Memgraph defaults to **implicit auto‑commit** transactions :contentReference[oaicite:1]{index=1},
7
+ # so we simply run the Cypher and return the rows.
8
+ def execute_cypher(cypher, params = {}, ctx = 'Query')
9
+ rows = run(cypher.gsub(/\belementId\(/i, 'id('), params, context: ctx)
10
+ process_records(rows)
11
+ end
12
+
13
+ # Implement database-specific methods for Memgraph
14
+
15
+ def convert_access_mode(mode)
16
+ # Memgraph doesn't distinguish between read/write modes
17
+ # but we'll keep the conversion here for consistency
18
+ mode.to_s
19
+ end
20
+
21
+ def prepare_tx_metadata(metadata, _db, _access_mode)
22
+ # Memgraph doesn't use db or access_mode in metadata
23
+ # but we'll ensure metadata is returned with compact
24
+ metadata.compact
25
+ end
26
+
27
+ protected
28
+
29
+ def protocol_handler_class = ProtocolHandler
30
+
31
+ def validate_connection
32
+ raise ActiveCypher::ConnectionError, "Server at #{config[:uri]} is not Memgraph" unless connection.server_agent.to_s.include?('Memgraph')
33
+
34
+ true
35
+ end
36
+
37
+ class ProtocolHandler < AbstractProtocolHandler
38
+ def extract_version(agent)
39
+ agent[%r{Memgraph/([\d.]+)}, 1] || 'unknown'
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveCypher
4
+ module ConnectionAdapters
5
+ class Neo4jAdapter < AbstractBoltAdapter
6
+ def execute_cypher(cypher, params = {}, ctx = 'Query')
7
+ connect
8
+ session = connection.session # thin wrapper around Bolt::Session
9
+ result = session.write_transaction do |tx|
10
+ logger.debug { "[#{ctx}] #{cypher} #{params.inspect}" }
11
+ tx.run(cypher, prepare_params(params))
12
+ end
13
+ process_records(result.to_a)
14
+ ensure
15
+ session&.close
16
+ end
17
+
18
+ # Explicit TX helpers — optional but handy.
19
+ def begin_transaction = (@tx = @connection.session.begin_transaction)
20
+ def commit_transaction(_) = @tx&.commit
21
+ def rollback_transaction(_) = @tx&.rollback
22
+
23
+ # Implement database-specific methods
24
+
25
+ def convert_access_mode(mode)
26
+ case mode.to_s
27
+ when 'r', 'read'
28
+ 'r'
29
+ when 'w', 'write'
30
+ 'w'
31
+ else
32
+ 'w' # Default to write
33
+ end
34
+ end
35
+
36
+ def prepare_tx_metadata(metadata, db, access_mode)
37
+ # Handle Neo4j-specific metadata
38
+ metadata['db'] = db if db
39
+ metadata['mode'] = convert_access_mode(access_mode)
40
+ metadata.compact
41
+ end
42
+
43
+ protected
44
+
45
+ def protocol_handler_class = ProtocolHandler
46
+
47
+ def validate_connection
48
+ raise ActiveCypher::ConnectionError, "Server at #{config[:uri]} is not Neo4j" unless connection.server_agent.to_s.include?('Neo4j/')
49
+
50
+ true
51
+ end
52
+
53
+ class ProtocolHandler < AbstractProtocolHandler
54
+ def extract_version(agent) = agent[%r{Neo4j/([\d.]+)}, 1] || 'unknown'
55
+ end
56
+ end
57
+ end
58
+ end