activecypher 0.10.4 → 0.11.1
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 +18 -1
- data/lib/active_cypher/bolt/connection.rb +421 -16
- data/lib/active_cypher/bolt/driver.rb +3 -17
- data/lib/active_cypher/bolt/session.rb +70 -50
- data/lib/active_cypher/bolt/transaction.rb +3 -10
- data/lib/active_cypher/connection_adapters/abstract_adapter.rb +6 -26
- data/lib/active_cypher/connection_adapters/abstract_bolt_adapter.rb +9 -1
- data/lib/active_cypher/connection_adapters/memgraph_adapter.rb +25 -2
- data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +25 -2
- data/lib/active_cypher/connection_pool.rb +11 -14
- data/lib/active_cypher/fixtures.rb +9 -21
- data/lib/active_cypher/instrumentation.rb +1 -1
- data/lib/active_cypher/logging.rb +1 -5
- data/lib/active_cypher/migrator.rb +0 -2
- data/lib/active_cypher/model/connection_owner.rb +2 -1
- data/lib/active_cypher/model/querying.rb +2 -26
- data/lib/active_cypher/relationship.rb +12 -17
- data/lib/active_cypher/version.rb +1 -1
- data/lib/cyrel/ast/clause_adapter.rb +4 -10
- data/lib/cyrel/ast/compiler.rb +5 -18
- data/lib/cyrel/logging.rb +0 -2
- data/lib/cyrel/query.rb +1 -0
- metadata +1 -3
- data/lib/active_cypher/model/inspectable.rb +0 -28
- data/lib/cyrel/ast/simple_cache.rb +0 -50
@@ -7,6 +7,7 @@ module ActiveCypher
|
|
7
7
|
# It maintains a connection to the database server and allows running queries.
|
8
8
|
class Session
|
9
9
|
include Instrumentation
|
10
|
+
|
10
11
|
attr_reader :connection, :database
|
11
12
|
|
12
13
|
def initialize(connection, database: nil)
|
@@ -120,53 +121,34 @@ module ActiveCypher
|
|
120
121
|
# @yield [tx] The transaction to use for queries.
|
121
122
|
# @return The result of the block.
|
122
123
|
def run_transaction(mode = :write, db: nil, timeout: nil, metadata: nil, &)
|
123
|
-
# Ensure we're running in an Async context
|
124
124
|
if Async::Task.current?
|
125
|
-
# Already in an
|
126
|
-
|
127
|
-
|
128
|
-
rescue StandardError => e
|
129
|
-
# Ensure errors are properly propagated
|
130
|
-
raise e
|
131
|
-
end
|
125
|
+
# Already in an async context, just run the block.
|
126
|
+
# The block will run asynchronously within the current task.
|
127
|
+
_execute_transaction_block(mode, db, timeout, metadata, &)
|
132
128
|
else
|
133
|
-
#
|
134
|
-
result = nil
|
135
|
-
error = nil
|
136
|
-
|
129
|
+
# Not in an async context, so we need to create one and wait for it to complete.
|
137
130
|
Async do
|
138
|
-
|
139
|
-
rescue StandardError => e
|
140
|
-
error = e
|
131
|
+
_execute_transaction_block(mode, db, timeout, metadata, &)
|
141
132
|
end.wait
|
142
|
-
|
143
|
-
# Re-raise any error outside the async block
|
144
|
-
raise error if error
|
145
|
-
|
146
|
-
result
|
147
133
|
end
|
148
134
|
end
|
149
135
|
|
150
|
-
#
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
tx.rollback
|
163
|
-
rescue StandardError => rollback_error
|
164
|
-
# Log rollback error but continue with the original error
|
165
|
-
puts "Error during rollback: #{rollback_error.message}" if ENV['DEBUG']
|
166
|
-
end
|
136
|
+
# Asynchronously execute a block of code within a transaction.
|
137
|
+
# This method is asynchronous and will return an `Async::Task` that will complete when the transaction is finished.
|
138
|
+
#
|
139
|
+
# @param mode [Symbol] The access mode (:read or :write).
|
140
|
+
# @param db [String] The database name to run the transaction against.
|
141
|
+
# @param timeout [Integer] Transaction timeout in milliseconds.
|
142
|
+
# @param metadata [Hash] Transaction metadata to send to the server.
|
143
|
+
# @yield [tx] The transaction to use for queries.
|
144
|
+
# @return [Async::Task] A task that will complete with the result of the block.
|
145
|
+
def async_run_transaction(mode = :write, db: nil, timeout: nil, metadata: nil, &)
|
146
|
+
# Ensure we are in an async task, otherwise the behavior is undefined.
|
147
|
+
raise 'Cannot run an async transaction outside of an Async task' unless Async::Task.current?
|
167
148
|
|
168
|
-
|
169
|
-
|
149
|
+
Async do
|
150
|
+
_execute_transaction_block(mode, db, timeout, metadata, &)
|
151
|
+
end
|
170
152
|
end
|
171
153
|
|
172
154
|
def write_transaction(db: nil, timeout: nil, metadata: nil, &)
|
@@ -177,6 +159,55 @@ module ActiveCypher
|
|
177
159
|
run_transaction(:read, db: db, timeout: timeout, metadata: metadata, &)
|
178
160
|
end
|
179
161
|
|
162
|
+
def async_write_transaction(db: nil, timeout: nil, metadata: nil, &)
|
163
|
+
async_run_transaction(:write, db: db, timeout: timeout, metadata: metadata, &)
|
164
|
+
end
|
165
|
+
|
166
|
+
def async_read_transaction(db: nil, timeout: nil, metadata: nil, &)
|
167
|
+
async_run_transaction(:read, db: db, timeout: timeout, metadata: metadata, &)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Close the session and any active transaction.
|
171
|
+
def close
|
172
|
+
instrument('session.close') do
|
173
|
+
# If there's an active transaction, try to roll it back
|
174
|
+
@current_transaction&.rollback if @current_transaction&.active?
|
175
|
+
|
176
|
+
# Mark current transaction as complete
|
177
|
+
complete_transaction(@current_transaction) if @current_transaction
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
def _execute_transaction_block(mode, db, timeout, metadata, &block)
|
184
|
+
tx = begin_transaction(db: db, access_mode: mode, tx_timeout: timeout, tx_metadata: metadata)
|
185
|
+
begin
|
186
|
+
result = block.call(tx)
|
187
|
+
tx.commit
|
188
|
+
result
|
189
|
+
rescue StandardError => e
|
190
|
+
# On any error, rollback the transaction and re-raise the original exception
|
191
|
+
begin
|
192
|
+
tx.rollback
|
193
|
+
rescue StandardError => rollback_error
|
194
|
+
# Log rollback error but continue with the original error
|
195
|
+
puts "Error during rollback: #{rollback_error.message}" if ENV['DEBUG']
|
196
|
+
end
|
197
|
+
|
198
|
+
# Reset the connection to ensure it's in a clean state for the next transaction
|
199
|
+
begin
|
200
|
+
@connection.reset!
|
201
|
+
rescue StandardError => reset_error
|
202
|
+
# If reset fails, the connection will be marked non-viable by the pool
|
203
|
+
puts "Error during connection reset: #{reset_error.message}" if ENV['DEBUG']
|
204
|
+
end
|
205
|
+
|
206
|
+
# Wrap the error in TransactionError to maintain compatibility
|
207
|
+
raise ActiveCypher::TransactionError, e.message
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
180
211
|
# Access the current bookmarks for this session.
|
181
212
|
def bookmarks
|
182
213
|
@bookmarks || []
|
@@ -197,17 +228,6 @@ module ActiveCypher
|
|
197
228
|
@connection.reset!
|
198
229
|
end
|
199
230
|
end
|
200
|
-
|
201
|
-
# Close the session and any active transaction.
|
202
|
-
def close
|
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?
|
206
|
-
|
207
|
-
# Mark current transaction as complete
|
208
|
-
complete_transaction(@current_transaction) if @current_transaction
|
209
|
-
end
|
210
|
-
end
|
211
231
|
end
|
212
232
|
end
|
213
233
|
end
|
@@ -5,6 +5,7 @@ module ActiveCypher
|
|
5
5
|
# Manages transaction state (BEGIN/COMMIT/ROLLBACK) and runs queries within a transaction.
|
6
6
|
class Transaction
|
7
7
|
include Instrumentation
|
8
|
+
|
8
9
|
attr_reader :bookmarks, :metadata, :connection
|
9
10
|
|
10
11
|
# Initializes a new Transaction instance.
|
@@ -147,11 +148,7 @@ module ActiveCypher
|
|
147
148
|
rescue ConnectionError
|
148
149
|
@state = :failed
|
149
150
|
# Mark transaction as completed in the session
|
150
|
-
|
151
|
-
@session.complete_transaction(self)
|
152
|
-
rescue StandardError
|
153
|
-
nil
|
154
|
-
end
|
151
|
+
@session.complete_transaction(self)
|
155
152
|
raise
|
156
153
|
end
|
157
154
|
|
@@ -184,11 +181,7 @@ module ActiveCypher
|
|
184
181
|
ensure
|
185
182
|
# Always mark as rolled back and complete the transaction
|
186
183
|
@state = :rolled_back
|
187
|
-
|
188
|
-
@session.complete_transaction(self)
|
189
|
-
rescue StandardError
|
190
|
-
nil
|
191
|
-
end
|
184
|
+
@session.complete_transaction(self)
|
192
185
|
end
|
193
186
|
end
|
194
187
|
|
@@ -49,32 +49,12 @@ module ActiveCypher
|
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
52
|
-
#
|
53
|
-
#
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
52
|
+
# Hydrates attributes from a database record
|
53
|
+
# @param record [Hash] The raw record from the database
|
54
|
+
# @param node_alias [Symbol] The alias used for the node in the query
|
55
|
+
# @return [Hash] The hydrated attributes
|
56
|
+
def hydrate_record(record, node_alias)
|
57
|
+
raise NotImplementedError, "#{self.class} must implement #hydrate_record"
|
78
58
|
end
|
79
59
|
|
80
60
|
# Turns rows into symbols, because Rubyists fear strings.
|
@@ -10,8 +10,16 @@ module ActiveCypher
|
|
10
10
|
# It's like ActiveRecord::ConnectionAdapter, but for weirdos like me who use graph databases.
|
11
11
|
class AbstractBoltAdapter < AbstractAdapter
|
12
12
|
include Instrumentation
|
13
|
+
|
13
14
|
attr_reader :connection
|
14
15
|
|
16
|
+
# Returns the raw Bolt connection object
|
17
|
+
# This is useful for accessing low-level connection methods like
|
18
|
+
# read_transaction, write_transaction, async_read_transaction, etc.
|
19
|
+
def raw_connection
|
20
|
+
@connection
|
21
|
+
end
|
22
|
+
|
15
23
|
# Establish a connection if not already active.
|
16
24
|
# This includes auth token prep, URI parsing, and quiet suffering.
|
17
25
|
def connect
|
@@ -64,7 +72,7 @@ module ActiveCypher
|
|
64
72
|
# Clean disconnection. Resets the internal state.
|
65
73
|
def disconnect
|
66
74
|
instrument_connection(:disconnect) do
|
67
|
-
@connection
|
75
|
+
@connection.close if @connection
|
68
76
|
@connection = nil
|
69
77
|
true
|
70
78
|
end
|
@@ -11,8 +11,6 @@ module ActiveCypher
|
|
11
11
|
def schema_catalog
|
12
12
|
rows = run('SHOW SCHEMA')
|
13
13
|
parse_schema(rows)
|
14
|
-
rescue StandardError
|
15
|
-
introspect_fallback
|
16
14
|
end
|
17
15
|
|
18
16
|
# Use id() for Memgraph instead of elementId()
|
@@ -108,6 +106,30 @@ module ActiveCypher
|
|
108
106
|
metadata.compact
|
109
107
|
end
|
110
108
|
|
109
|
+
# Hydrates attributes from a Memgraph record
|
110
|
+
# @param record [Hash] The raw record from Memgraph
|
111
|
+
# @param node_alias [Symbol] The alias used for the node in the query
|
112
|
+
# @return [Hash] The hydrated attributes
|
113
|
+
def hydrate_record(record, node_alias)
|
114
|
+
attrs = {}
|
115
|
+
node_data = record[node_alias] || record[node_alias.to_s]
|
116
|
+
|
117
|
+
if node_data.is_a?(Array) && node_data.length >= 2
|
118
|
+
properties_container = node_data[1]
|
119
|
+
if properties_container.is_a?(Array) && properties_container.length >= 3
|
120
|
+
properties = properties_container[2]
|
121
|
+
properties.each { |k, v| attrs[k.to_sym] = v } if properties.is_a?(Hash)
|
122
|
+
end
|
123
|
+
elsif node_data.is_a?(Hash)
|
124
|
+
node_data.each { |k, v| attrs[k.to_sym] = v }
|
125
|
+
elsif node_data.respond_to?(:properties)
|
126
|
+
attrs = node_data.properties.symbolize_keys
|
127
|
+
end
|
128
|
+
|
129
|
+
attrs[:internal_id] = record[:internal_id] || record['internal_id']
|
130
|
+
attrs
|
131
|
+
end
|
132
|
+
|
111
133
|
protected
|
112
134
|
|
113
135
|
def parse_schema(rows)
|
@@ -174,6 +196,7 @@ module ActiveCypher
|
|
174
196
|
|
175
197
|
module Persistence
|
176
198
|
include PersistenceMethods
|
199
|
+
|
177
200
|
module_function :create_record, :update_record, :destroy_record
|
178
201
|
end
|
179
202
|
|
@@ -33,8 +33,6 @@ module ActiveCypher
|
|
33
33
|
|
34
34
|
Schema::Catalog.new(indexes: idx_defs, constraints: con_defs,
|
35
35
|
node_types: [], edge_types: [])
|
36
|
-
rescue StandardError
|
37
|
-
Schema::Catalog.new(indexes: [], constraints: [], node_types: [], edge_types: [])
|
38
36
|
end
|
39
37
|
|
40
38
|
# Use elementId() for Neo4j
|
@@ -130,8 +128,33 @@ module ActiveCypher
|
|
130
128
|
metadata.compact
|
131
129
|
end
|
132
130
|
|
131
|
+
# Hydrates attributes from a Neo4j record
|
132
|
+
# @param record [Hash] The raw record from Neo4j
|
133
|
+
# @param node_alias [Symbol] The alias used for the node in the query
|
134
|
+
# @return [Hash] The hydrated attributes
|
135
|
+
def hydrate_record(record, node_alias)
|
136
|
+
attrs = {}
|
137
|
+
node_data = record[node_alias] || record[node_alias.to_s]
|
138
|
+
|
139
|
+
if node_data.is_a?(Array) && node_data.length >= 2
|
140
|
+
properties_container = node_data[1]
|
141
|
+
if properties_container.is_a?(Array) && properties_container.length >= 3
|
142
|
+
properties = properties_container[2]
|
143
|
+
properties.each { |k, v| attrs[k.to_sym] = v } if properties.is_a?(Hash)
|
144
|
+
end
|
145
|
+
elsif node_data.is_a?(Hash)
|
146
|
+
node_data.each { |k, v| attrs[k.to_sym] = v }
|
147
|
+
elsif node_data.respond_to?(:properties)
|
148
|
+
attrs = node_data.properties.symbolize_keys
|
149
|
+
end
|
150
|
+
|
151
|
+
attrs[:internal_id] = record[:internal_id] || record['internal_id']
|
152
|
+
attrs
|
153
|
+
end
|
154
|
+
|
133
155
|
module Persistence
|
134
156
|
include PersistenceMethods
|
157
|
+
|
135
158
|
module_function :create_record, :update_record, :destroy_record
|
136
159
|
end
|
137
160
|
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'concurrent/atomic/atomic_reference'
|
4
3
|
require 'timeout'
|
5
4
|
|
6
5
|
module ActiveCypher
|
@@ -24,29 +23,27 @@ module ActiveCypher
|
|
24
23
|
|
25
24
|
# Merge the resolved config with any additional options
|
26
25
|
@spec = resolved_config.merge(@spec.except(:url))
|
27
|
-
|
28
26
|
end
|
29
27
|
|
30
|
-
@conn_ref =
|
28
|
+
@conn_ref = nil # holds the adapter instance
|
31
29
|
@creation_mutex = Mutex.new # prevents multiple threads from creating connections simultaneously
|
32
|
-
@retry_count = Concurrent::AtomicReference.new(0)
|
33
30
|
end
|
34
31
|
|
35
|
-
# Returns a live adapter,
|
32
|
+
# Returns a live adapter, initializing it once in a thread‑safe way.
|
36
33
|
def connection
|
37
|
-
# Fast path —already connected and alive
|
38
|
-
conn = @conn_ref
|
34
|
+
# Fast path — already connected and alive
|
35
|
+
conn = @conn_ref
|
39
36
|
return conn if conn&.active?
|
40
37
|
|
41
38
|
# Use mutex for the slow path to prevent thundering herd
|
42
39
|
@creation_mutex.synchronize do
|
43
40
|
# Check again inside the mutex in case another thread created it
|
44
|
-
conn = @conn_ref
|
41
|
+
conn = @conn_ref
|
45
42
|
return conn if conn&.active?
|
46
43
|
|
47
44
|
# Create a new connection
|
48
45
|
new_conn = build_connection
|
49
|
-
@conn_ref
|
46
|
+
@conn_ref = new_conn
|
50
47
|
return new_conn
|
51
48
|
end
|
52
49
|
end
|
@@ -54,12 +51,12 @@ module ActiveCypher
|
|
54
51
|
|
55
52
|
# Check if the pool has a persistent connection issue
|
56
53
|
def troubled?
|
57
|
-
@retry_count
|
54
|
+
@retry_count >= @spec[:max_retries]
|
58
55
|
end
|
59
56
|
|
60
57
|
# Explicitly close and reset the connection
|
61
58
|
def disconnect
|
62
|
-
conn = @conn_ref
|
59
|
+
conn = @conn_ref
|
63
60
|
return unless conn
|
64
61
|
|
65
62
|
begin
|
@@ -68,7 +65,7 @@ module ActiveCypher
|
|
68
65
|
# Log but don't raise to ensure cleanup continues
|
69
66
|
puts "Warning: Error disconnecting: #{e.message}" if ENV['DEBUG']
|
70
67
|
ensure
|
71
|
-
@conn_ref
|
68
|
+
@conn_ref = nil
|
72
69
|
end
|
73
70
|
end
|
74
71
|
|
@@ -91,8 +88,8 @@ module ActiveCypher
|
|
91
88
|
rescue Timeout::Error
|
92
89
|
begin
|
93
90
|
adapter.disconnect
|
94
|
-
rescue StandardError
|
95
|
-
|
91
|
+
rescue StandardError => e
|
92
|
+
puts "Warning: Error disconnecting during timeout cleanup: #{e.message}" if ENV['DEBUG']
|
96
93
|
end
|
97
94
|
raise ConnectionError, "Connection timed out after #{@spec[:pool_timeout]} seconds"
|
98
95
|
end
|
@@ -32,11 +32,7 @@ module ActiveCypher
|
|
32
32
|
|
33
33
|
# 5. Gather unique connections for all model classes referenced in this profile
|
34
34
|
model_classes = dsl_context.nodes.map { |node| node[:model_class] }.uniq
|
35
|
-
connections = model_classes.map
|
36
|
-
klass.connection
|
37
|
-
rescue StandardError
|
38
|
-
nil
|
39
|
-
end.compact.uniq
|
35
|
+
connections = model_classes.map(&:connection).compact.uniq
|
40
36
|
|
41
37
|
# 6. Wipe all nodes in each relevant connection
|
42
38
|
connections.each do |conn|
|
@@ -78,11 +74,7 @@ module ActiveCypher
|
|
78
74
|
end
|
79
75
|
|
80
76
|
# Gather unique connections from all model classes
|
81
|
-
connections = model_classes.map
|
82
|
-
klass.connection
|
83
|
-
rescue StandardError
|
84
|
-
nil
|
85
|
-
end.compact.uniq
|
77
|
+
connections = model_classes.map(&:connection).compact.uniq
|
86
78
|
|
87
79
|
# Wipe all nodes in each connection
|
88
80
|
connections.each do |conn|
|
@@ -104,17 +96,13 @@ module ActiveCypher
|
|
104
96
|
next unless klass < ActiveCypher::Base
|
105
97
|
next if klass.respond_to?(:abstract_class?) && klass.abstract_class?
|
106
98
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
}
|
115
|
-
rescue StandardError
|
116
|
-
# Skip if can't get connection
|
117
|
-
end
|
99
|
+
conn = klass.connection
|
100
|
+
# Store connection details for comparison
|
101
|
+
model_connections[klass] = {
|
102
|
+
adapter: conn.class.name,
|
103
|
+
config: conn.instance_variable_get(:@config),
|
104
|
+
object_id: conn.object_id
|
105
|
+
}
|
118
106
|
end
|
119
107
|
|
120
108
|
relationships.each do |rel|
|
@@ -132,7 +132,7 @@ module ActiveCypher
|
|
132
132
|
# @param key [String, Symbol] The key to check
|
133
133
|
# @return [Boolean] True if the key contains sensitive information
|
134
134
|
def sensitive_key?(key)
|
135
|
-
return true if key.to_s.match?(/(^|[
|
135
|
+
return true if key.to_s.match?(/(^|[-_])(?:password|token|secret|credential|key)($|[-_])/i)
|
136
136
|
|
137
137
|
# Check against Rails filter parameters if available
|
138
138
|
if defined?(Rails) && Rails.application
|
@@ -14,11 +14,7 @@ module ActiveCypher
|
|
14
14
|
self.backend ||= begin
|
15
15
|
base = Logger.new($stdout)
|
16
16
|
base.level = ENV.fetch('AC_LOG_LEVEL', 'info').upcase
|
17
|
-
.then
|
18
|
-
Logger.const_get(lvl)
|
19
|
-
rescue StandardError
|
20
|
-
Logger::INFO
|
21
|
-
end
|
17
|
+
.then { |lvl| Logger.const_get(lvl) }
|
22
18
|
ActiveSupport::TaggedLogging.new(base).tap { |l| l.tagged! 'ActiveCypher' }
|
23
19
|
end
|
24
20
|
end
|
@@ -39,9 +39,7 @@ module ActiveCypher
|
|
39
39
|
adapter = connection.id_handler
|
40
40
|
label_string = labels.map { |l| ":#{l}" }.join
|
41
41
|
|
42
|
-
# Handle ID format based on adapter
|
43
|
-
# Neo4j insists on string IDs like "4:uuid:wtf" because simple integers are for peasants
|
44
|
-
# Memgraph keeps it real with numeric IDs because it doesn't need to prove anything
|
42
|
+
# Handle ID format based on adapter type
|
45
43
|
formatted_id = if adapter.id_function == 'elementId'
|
46
44
|
internal_db_id.to_s # String for Neo4j
|
47
45
|
else
|
@@ -59,7 +57,7 @@ module ActiveCypher
|
|
59
57
|
record = result.first
|
60
58
|
|
61
59
|
if record
|
62
|
-
attrs =
|
60
|
+
attrs = connection.hydrate_record(record, node_alias)
|
63
61
|
return instantiate(attrs)
|
64
62
|
end
|
65
63
|
|
@@ -97,28 +95,6 @@ module ActiveCypher
|
|
97
95
|
# @return [Object] The new, possibly persisted record
|
98
96
|
# Because sometimes you just want to live dangerously.
|
99
97
|
def create(attrs = {}) = new(attrs).tap(&:save)
|
100
|
-
|
101
|
-
private
|
102
|
-
|
103
|
-
def _hydrate_attributes_from_memgraph_record(record, node_alias)
|
104
|
-
attrs = {}
|
105
|
-
node_data = record[node_alias] || record[node_alias.to_s]
|
106
|
-
|
107
|
-
if node_data.is_a?(Array) && node_data.length >= 2
|
108
|
-
properties_container = node_data[1]
|
109
|
-
if properties_container.is_a?(Array) && properties_container.length >= 3
|
110
|
-
properties = properties_container[2]
|
111
|
-
properties.each { |k, v| attrs[k.to_sym] = v } if properties.is_a?(Hash)
|
112
|
-
end
|
113
|
-
elsif node_data.is_a?(Hash)
|
114
|
-
node_data.each { |k, v| attrs[k.to_sym] = v }
|
115
|
-
elsif node_data.respond_to?(:properties)
|
116
|
-
attrs = node_data.properties.symbolize_keys
|
117
|
-
end
|
118
|
-
|
119
|
-
attrs[:internal_id] = record[:internal_id] || record['internal_id']
|
120
|
-
attrs
|
121
|
-
end
|
122
98
|
end
|
123
99
|
end
|
124
100
|
end
|
@@ -73,11 +73,7 @@ module ActiveCypher
|
|
73
73
|
|
74
74
|
return @connection if defined?(@connection) && @connection
|
75
75
|
|
76
|
-
|
77
|
-
from_class.constantize.connection
|
78
|
-
rescue StandardError
|
79
|
-
nil
|
80
|
-
end
|
76
|
+
from_class.constantize.connection
|
81
77
|
end
|
82
78
|
|
83
79
|
# --------------------------------------------------------------
|
@@ -301,9 +297,14 @@ module ActiveCypher
|
|
301
297
|
_run(:update) { update_relationship }
|
302
298
|
end
|
303
299
|
end
|
304
|
-
rescue
|
305
|
-
|
306
|
-
|
300
|
+
rescue ActiveCypher::RecordNotSaved, RuntimeError => e
|
301
|
+
# Only catch specific validation errors, let other errors propagate
|
302
|
+
if e.message.include?('must be persisted') || e.message.include?('creation returned no id')
|
303
|
+
log_error "Failed to save #{self.class}: #{e.message}"
|
304
|
+
false
|
305
|
+
else
|
306
|
+
raise
|
307
|
+
end
|
307
308
|
end
|
308
309
|
|
309
310
|
# Bang version of save - raises exception if save fails
|
@@ -366,16 +367,14 @@ module ActiveCypher
|
|
366
367
|
|
367
368
|
props = attributes.except('internal_id').compact
|
368
369
|
rel_ty = self.class.relationship_type
|
369
|
-
arrow = '->' # outgoing by default
|
370
|
-
|
371
370
|
adapter = self.class.connection.id_handler
|
372
|
-
parts = []
|
373
371
|
|
374
372
|
# Build the Cypher query based on the adapter
|
375
373
|
id_clause = adapter.with_direct_node_ids(from_node.internal_id, to_node.internal_id)
|
374
|
+
parts = []
|
376
375
|
parts << "MATCH (p), (h) WHERE #{id_clause}"
|
377
|
-
parts << "CREATE (p)-[r:#{rel_ty}]
|
378
|
-
parts << 'SET r += $props' unless props.empty?
|
376
|
+
parts << "CREATE (p)-[r:#{rel_ty}]->(h)"
|
377
|
+
parts << 'SET r += $props' unless props.empty?
|
379
378
|
parts << "RETURN #{adapter.return_id}"
|
380
379
|
|
381
380
|
cypher = parts.join(' ')
|
@@ -383,7 +382,6 @@ module ActiveCypher
|
|
383
382
|
|
384
383
|
# Execute Cypher query
|
385
384
|
result = self.class.connection.execute_cypher(cypher, params, 'Create Relationship')
|
386
|
-
|
387
385
|
row = result.first
|
388
386
|
|
389
387
|
# Try different ways to access the ID
|
@@ -399,9 +397,6 @@ module ActiveCypher
|
|
399
397
|
@new_record = false
|
400
398
|
changes_applied
|
401
399
|
true
|
402
|
-
rescue StandardError => e
|
403
|
-
log_error "Failed to save #{self.class}: #{e.class} – #{e.message}"
|
404
|
-
false
|
405
400
|
end
|
406
401
|
|
407
402
|
def update_relationship
|
@@ -9,20 +9,14 @@ module Cyrel
|
|
9
9
|
|
10
10
|
def initialize(ast_node)
|
11
11
|
@ast_node = ast_node
|
12
|
-
@ast_node_hash = ast_node.hash
|
13
12
|
super()
|
14
13
|
end
|
15
14
|
|
16
15
|
def render(query)
|
17
|
-
#
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
# Create a compiler that delegates parameter registration to the query
|
22
|
-
compiler = QueryIntegratedCompiler.new(query)
|
23
|
-
compiler.compile(ast_node)
|
24
|
-
compiler.output.string
|
25
|
-
end
|
16
|
+
# Create a compiler that delegates parameter registration to the query
|
17
|
+
compiler = QueryIntegratedCompiler.new(query)
|
18
|
+
compiler.compile(ast_node)
|
19
|
+
compiler.output.string
|
26
20
|
end
|
27
21
|
|
28
22
|
# Ruby 3.0+ pattern matching support
|