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.
- checksums.yaml +4 -4
- data/lib/active_cypher/base.rb +28 -8
- data/lib/active_cypher/bolt/driver.rb +6 -16
- data/lib/active_cypher/bolt/session.rb +62 -50
- data/lib/active_cypher/bolt/transaction.rb +95 -90
- data/lib/active_cypher/connection_adapters/abstract_adapter.rb +28 -0
- data/lib/active_cypher/connection_adapters/abstract_bolt_adapter.rb +40 -32
- data/lib/active_cypher/connection_adapters/memgraph_adapter.rb +44 -0
- data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +27 -1
- data/lib/active_cypher/connection_adapters/registry.rb +94 -0
- data/lib/active_cypher/connection_handler.rb +18 -3
- data/lib/active_cypher/connection_pool.rb +5 -23
- data/lib/active_cypher/connection_url_resolver.rb +14 -3
- data/lib/active_cypher/cypher_config.rb +2 -1
- data/lib/active_cypher/fixtures/dsl_context.rb +41 -0
- data/lib/active_cypher/fixtures/evaluator.rb +37 -0
- data/lib/active_cypher/fixtures/node_builder.rb +53 -0
- data/lib/active_cypher/fixtures/parser.rb +23 -0
- data/lib/active_cypher/fixtures/registry.rb +43 -0
- data/lib/active_cypher/fixtures/rel_builder.rb +96 -0
- data/lib/active_cypher/fixtures.rb +177 -0
- data/lib/active_cypher/generators/node_generator.rb +32 -3
- data/lib/active_cypher/generators/relationship_generator.rb +29 -2
- data/lib/active_cypher/generators/templates/application_graph_node.rb +1 -0
- data/lib/active_cypher/generators/templates/node.rb.erb +10 -6
- data/lib/active_cypher/generators/templates/relationship.rb.erb +7 -6
- data/lib/active_cypher/instrumentation.rb +186 -0
- data/lib/active_cypher/model/callbacks.rb +5 -13
- data/lib/active_cypher/model/connection_handling.rb +37 -52
- data/lib/active_cypher/model/connection_owner.rb +41 -33
- data/lib/active_cypher/model/core.rb +4 -12
- data/lib/active_cypher/model/countable.rb +10 -3
- data/lib/active_cypher/model/destruction.rb +23 -18
- data/lib/active_cypher/model/labelling.rb +45 -0
- data/lib/active_cypher/model/persistence.rb +52 -26
- data/lib/active_cypher/model/querying.rb +49 -25
- data/lib/active_cypher/railtie.rb +40 -5
- data/lib/active_cypher/relation.rb +10 -2
- data/lib/active_cypher/relationship.rb +77 -17
- data/lib/active_cypher/version.rb +1 -1
- data/lib/activecypher.rb +4 -1
- data/lib/cyrel/clause/set.rb +20 -10
- data/lib/cyrel/expression/property_access.rb +2 -0
- data/lib/cyrel/functions.rb +3 -1
- data/lib/cyrel/logging.rb +43 -0
- data/lib/cyrel/plus.rb +11 -0
- data/lib/cyrel/query.rb +7 -1
- data/lib/cyrel.rb +77 -18
- metadata +13 -2
- data/lib/active_cypher/connection_factory.rb +0 -130
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6116df314b95f0db74a73fcc1746f0e4bf9bd529d65ab04e806953103b4017b0
|
4
|
+
data.tar.gz: d85d13cbb5ad5a9b7434f3457d4dd3b2f899c47619dbdf00f129790466b19fbf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1e12458b40696e189c20cf480565446c2d2294cd2fe2b8208560dbb77ca27f38b4971f043c11531f229dee070b5950ca785906660e121b25f74d3157f89b3a38
|
7
|
+
data.tar.gz: 7dd4fbe5fc47b350789f7f55e55321238626e7bed731d56bb83d37417411f257fc53cb0b3a2223292ac64f9b971fd07d5d9a8f9fe666b8d49c872d83a9af60de
|
data/lib/active_cypher/base.rb
CHANGED
@@ -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
|
-
|
33
|
-
|
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
|
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
|
-
|
33
|
-
|
34
|
-
|
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 =
|
39
|
-
@verify_cert =
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
#
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
-
|
185
|
-
|
192
|
+
instrument('session.reset') do
|
193
|
+
# Mark the current transaction as no longer active
|
194
|
+
complete_transaction(@current_transaction)
|
186
195
|
|
187
|
-
|
188
|
-
|
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
|
-
|
194
|
-
|
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
|
-
|
197
|
-
|
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
|
-
|
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
|
-
#
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
110
|
-
|
113
|
+
# Read response to COMMIT
|
114
|
+
response = connection.read_message
|
111
115
|
|
112
|
-
|
113
|
-
|
114
|
-
|
116
|
+
case response
|
117
|
+
when Messaging::Success
|
118
|
+
# COMMIT succeeded
|
115
119
|
|
116
|
-
|
120
|
+
@state = :committed
|
117
121
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
126
|
-
|
129
|
+
# Mark transaction as completed in the session
|
130
|
+
@session.complete_transaction(self, new_bookmarks)
|
127
131
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
-
|
136
|
-
|
139
|
+
# Mark transaction as completed in the session
|
140
|
+
@session.complete_transaction(self)
|
137
141
|
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
163
|
+
instrument_transaction(:rollback, object_id) do
|
159
164
|
# Send ROLLBACK message
|
160
165
|
rollback_msg = Messaging::Rollback.new
|
161
|
-
|
166
|
+
connection.write_message(rollback_msg)
|
162
167
|
|
163
168
|
# Read response to ROLLBACK
|
164
|
-
response =
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
|