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.
- checksums.yaml +4 -4
- data/lib/active_cypher/associations/collection_proxy.rb +144 -0
- data/lib/active_cypher/associations.rb +537 -0
- data/lib/active_cypher/base.rb +47 -0
- data/lib/active_cypher/bolt/connection.rb +525 -0
- data/lib/active_cypher/bolt/driver.rb +144 -0
- data/lib/active_cypher/bolt/handlers.rb +10 -0
- data/lib/active_cypher/bolt/message_reader.rb +100 -0
- data/lib/active_cypher/bolt/message_writer.rb +53 -0
- data/lib/active_cypher/bolt/messaging.rb +307 -0
- data/lib/active_cypher/bolt/packstream.rb +319 -0
- data/lib/active_cypher/bolt/result.rb +82 -0
- data/lib/active_cypher/bolt/session.rb +201 -0
- data/lib/active_cypher/bolt/transaction.rb +211 -0
- data/lib/active_cypher/bolt/version_encoding.rb +41 -0
- data/lib/active_cypher/bolt.rb +7 -0
- data/lib/active_cypher/connection_adapters/abstract_adapter.rb +75 -0
- data/lib/active_cypher/connection_adapters/abstract_bolt_adapter.rb +178 -0
- data/lib/active_cypher/connection_adapters/memgraph_adapter.rb +44 -0
- data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +58 -0
- data/lib/active_cypher/connection_factory.rb +130 -0
- data/lib/active_cypher/connection_handler.rb +9 -0
- data/lib/active_cypher/connection_pool.rb +123 -0
- data/lib/active_cypher/connection_url_resolver.rb +137 -0
- data/lib/active_cypher/cypher_config.rb +50 -0
- data/lib/active_cypher/generators/install_generator.rb +23 -0
- data/lib/active_cypher/generators/node_generator.rb +32 -0
- data/lib/active_cypher/generators/relationship_generator.rb +33 -0
- data/lib/active_cypher/generators/templates/application_graph_node.rb +5 -0
- data/lib/active_cypher/generators/templates/application_graph_relationship.rb +5 -0
- data/lib/active_cypher/generators/templates/cypher_databases.yml +28 -0
- data/lib/active_cypher/generators/templates/node.rb.erb +10 -0
- data/lib/active_cypher/generators/templates/relationship.rb.erb +11 -0
- data/lib/active_cypher/logging.rb +44 -0
- data/lib/active_cypher/model/abstract.rb +87 -0
- data/lib/active_cypher/model/attributes.rb +24 -0
- data/lib/active_cypher/model/callbacks.rb +44 -0
- data/lib/active_cypher/model/connection_handling.rb +76 -0
- data/lib/active_cypher/model/connection_owner.rb +50 -0
- data/lib/active_cypher/model/core.rb +45 -0
- data/lib/active_cypher/model/countable.rb +30 -0
- data/lib/active_cypher/model/destruction.rb +49 -0
- data/lib/active_cypher/model/inspectable.rb +28 -0
- data/lib/active_cypher/model/persistence.rb +182 -0
- data/lib/active_cypher/model/querying.rb +67 -0
- data/lib/active_cypher/railtie.rb +34 -0
- data/lib/active_cypher/relation.rb +190 -0
- data/lib/active_cypher/relationship.rb +233 -0
- data/lib/active_cypher/runtime_registry.rb +8 -0
- data/lib/active_cypher/scoping.rb +97 -0
- data/lib/active_cypher/utils/logger.rb +100 -0
- data/lib/active_cypher/version.rb +5 -0
- data/lib/activecypher.rb +108 -0
- data/lib/cyrel/call_procedure.rb +29 -0
- data/lib/cyrel/clause/call.rb +46 -0
- data/lib/cyrel/clause/call_subquery.rb +40 -0
- data/lib/cyrel/clause/create.rb +33 -0
- data/lib/cyrel/clause/delete.rb +41 -0
- data/lib/cyrel/clause/limit.rb +33 -0
- data/lib/cyrel/clause/match.rb +40 -0
- data/lib/cyrel/clause/merge.rb +34 -0
- data/lib/cyrel/clause/order_by.rb +78 -0
- data/lib/cyrel/clause/remove.rb +75 -0
- data/lib/cyrel/clause/return.rb +90 -0
- data/lib/cyrel/clause/set.rb +97 -0
- data/lib/cyrel/clause/skip.rb +34 -0
- data/lib/cyrel/clause/where.rb +42 -0
- data/lib/cyrel/clause/with.rb +94 -0
- data/lib/cyrel/clause.rb +25 -0
- data/lib/cyrel/direction.rb +18 -0
- data/lib/cyrel/expression/alias.rb +27 -0
- data/lib/cyrel/expression/base.rb +101 -0
- data/lib/cyrel/expression/case.rb +45 -0
- data/lib/cyrel/expression/comparison.rb +60 -0
- data/lib/cyrel/expression/exists.rb +42 -0
- data/lib/cyrel/expression/function_call.rb +57 -0
- data/lib/cyrel/expression/literal.rb +33 -0
- data/lib/cyrel/expression/logical.rb +38 -0
- data/lib/cyrel/expression/operator.rb +27 -0
- data/lib/cyrel/expression/pattern_comprehension.rb +44 -0
- data/lib/cyrel/expression/property_access.rb +25 -0
- data/lib/cyrel/expression.rb +56 -0
- data/lib/cyrel/functions.rb +116 -0
- data/lib/cyrel/node.rb +397 -0
- data/lib/cyrel/parameterizable.rb +20 -0
- data/lib/cyrel/pattern/node.rb +66 -0
- data/lib/cyrel/pattern/path.rb +41 -0
- data/lib/cyrel/pattern/relationship.rb +74 -0
- data/lib/cyrel/pattern.rb +8 -0
- data/lib/cyrel/query.rb +497 -0
- data/lib/cyrel/return_only.rb +26 -0
- data/lib/cyrel/types/hash_type.rb +22 -0
- data/lib/cyrel/types/symbol_type.rb +13 -0
- data/lib/cyrel.rb +72 -0
- data/lib/tasks/active_cypher_tasks.rake +6 -0
- data/sig/activecypher.rbs +4 -0
- 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,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
|