activecypher 0.7.0 → 0.7.2
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/connection_adapters/abstract_bolt_adapter.rb +50 -0
- data/lib/active_cypher/connection_adapters/memgraph_adapter.rb +29 -3
- data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +6 -2
- data/lib/active_cypher/connection_adapters/persistence_methods.rb +77 -0
- data/lib/active_cypher/model/destruction.rb +3 -13
- data/lib/active_cypher/model/persistence.rb +7 -62
- data/lib/active_cypher/relationship.rb +3 -0
- data/lib/active_cypher/schema/catalog.rb +2 -0
- data/lib/active_cypher/schema/dumper.rb +4 -2
- data/lib/active_cypher/schema/writer/cypher.rb +2 -0
- data/lib/active_cypher/version.rb +1 -1
- data/lib/cyrel/pattern/node.rb +1 -1
- data/lib/tasks/graphdb_migrate.rake +3 -1
- data/lib/tasks/graphdb_schema.rake +10 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 738573111aeb926ef359c2bd2643e07d25f83b76d63f7cebf0d31e762c06fabf
|
4
|
+
data.tar.gz: 8ede3884325f7e86e4ca06dea636e0193d8742fc19b0ed2e92724f3270f31f7f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: afc1446c1c2e7195ed3ad241e84bcdaffaf9c634084068088f15137f897ea4955d80448af5b80b225af204a23baa217486b4327160b50b7bd41899d7c6b95df1
|
7
|
+
data.tar.gz: f5cf3960f8127672e803d9dd9d2246e8245499e2f7208013e2bd97a4e5971fbcd5044aa107bcf1208a21cfc40337d0c89cbc3354706dce06f0bb1df8b62d84a3
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'uri'
|
4
|
+
require 'async'
|
4
5
|
|
5
6
|
module ActiveCypher
|
6
7
|
module ConnectionAdapters
|
@@ -99,6 +100,55 @@ module ActiveCypher
|
|
99
100
|
# Return handler for connection to store
|
100
101
|
end
|
101
102
|
|
103
|
+
# Reset the connection state by sending a RESET message.
|
104
|
+
# This clears any pending work and returns the connection to a clean state.
|
105
|
+
# Useful for error recovery or connection pooling.
|
106
|
+
#
|
107
|
+
# @return [Boolean] true if reset succeeded, false otherwise
|
108
|
+
def reset!
|
109
|
+
return false unless active?
|
110
|
+
|
111
|
+
instrument_connection(:reset, config) do
|
112
|
+
# Wrap in async to handle the connection reset properly
|
113
|
+
result = nil
|
114
|
+
error = nil
|
115
|
+
|
116
|
+
Async do
|
117
|
+
begin
|
118
|
+
# Try to execute a simple query first
|
119
|
+
session = Bolt::Session.new(@connection)
|
120
|
+
session.run('RETURN 1 AS check', {})
|
121
|
+
session.close
|
122
|
+
result = true
|
123
|
+
rescue StandardError => e
|
124
|
+
# Query failed, need to reset the connection
|
125
|
+
logger.debug { "Connection needs reset: #{e.message}" }
|
126
|
+
|
127
|
+
# Send RESET message directly
|
128
|
+
begin
|
129
|
+
@connection.write_message(Bolt::Messaging::Reset.new)
|
130
|
+
response = @connection.read_message
|
131
|
+
result = response.is_a?(Bolt::Messaging::Success)
|
132
|
+
logger.debug { "Reset response: #{response.class}" }
|
133
|
+
rescue StandardError => reset_error
|
134
|
+
logger.error { "Reset failed: #{reset_error.message}" }
|
135
|
+
result = false
|
136
|
+
end
|
137
|
+
end
|
138
|
+
rescue StandardError => e
|
139
|
+
error = e
|
140
|
+
end.wait
|
141
|
+
|
142
|
+
raise error if error
|
143
|
+
|
144
|
+
result
|
145
|
+
end
|
146
|
+
rescue StandardError => e
|
147
|
+
# This is madness!
|
148
|
+
logger.error { "Failed to reset connection: #{e.message}" }
|
149
|
+
false
|
150
|
+
end
|
151
|
+
|
102
152
|
protected
|
103
153
|
|
104
154
|
# These must be defined by subclasses. If you don't override them,
|
@@ -1,5 +1,4 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
require "active_cypher/schema/catalog"
|
3
2
|
|
4
3
|
module ActiveCypher
|
5
4
|
module ConnectionAdapters
|
@@ -45,7 +44,26 @@ module ActiveCypher
|
|
45
44
|
self.class
|
46
45
|
end
|
47
46
|
|
48
|
-
#
|
47
|
+
# Override run to execute queries without explicit transactions
|
48
|
+
# Memgraph auto‑commits each query, so we send RUN + PULL directly
|
49
|
+
def run(cypher, params = {}, context: 'Query', db: nil, access_mode: :write)
|
50
|
+
connect
|
51
|
+
logger.debug { "[#{context}] #{cypher} #{params.inspect}" }
|
52
|
+
|
53
|
+
instrument_query(cypher, params, context: context, metadata: { db: db, access_mode: access_mode }) do
|
54
|
+
session = Bolt::Session.new(connection)
|
55
|
+
|
56
|
+
rows = session.run_transaction(access_mode, db: db) do |tx|
|
57
|
+
result = tx.run(cypher, prepare_params(params))
|
58
|
+
result.respond_to?(:to_a) ? result.to_a : result
|
59
|
+
end
|
60
|
+
|
61
|
+
session.close
|
62
|
+
rows
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Memgraph defaults to **implicit auto‑commit** transactions
|
49
67
|
# so we simply run the Cypher and return the rows.
|
50
68
|
def execute_cypher(cypher, params = {}, ctx = 'Query')
|
51
69
|
rows = run(cypher.gsub(/\belementId\(/i, 'id('), params, context: ctx)
|
@@ -69,7 +87,10 @@ module ActiveCypher
|
|
69
87
|
protected
|
70
88
|
|
71
89
|
def parse_schema(rows)
|
72
|
-
nodes
|
90
|
+
nodes = []
|
91
|
+
edges = []
|
92
|
+
idx = []
|
93
|
+
cons = []
|
73
94
|
|
74
95
|
rows.each do |row|
|
75
96
|
case row['type']
|
@@ -118,6 +139,11 @@ module ActiveCypher
|
|
118
139
|
end
|
119
140
|
end
|
120
141
|
|
142
|
+
module Persistence
|
143
|
+
include PersistenceMethods
|
144
|
+
module_function :create_record, :update_record, :destroy_record
|
145
|
+
end
|
146
|
+
|
121
147
|
class ProtocolHandler < AbstractProtocolHandler
|
122
148
|
def extract_version(agent)
|
123
149
|
agent[%r{Memgraph/([\d.]+)}, 1] || 'unknown'
|
@@ -1,5 +1,4 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
require "active_cypher/schema/catalog"
|
3
2
|
|
4
3
|
module ActiveCypher
|
5
4
|
module ConnectionAdapters
|
@@ -33,7 +32,7 @@ module ActiveCypher
|
|
33
32
|
end
|
34
33
|
|
35
34
|
Schema::Catalog.new(indexes: idx_defs, constraints: con_defs,
|
36
|
-
|
35
|
+
node_types: [], edge_types: [])
|
37
36
|
rescue StandardError
|
38
37
|
Schema::Catalog.new(indexes: [], constraints: [], node_types: [], edge_types: [])
|
39
38
|
end
|
@@ -99,6 +98,11 @@ module ActiveCypher
|
|
99
98
|
metadata.compact
|
100
99
|
end
|
101
100
|
|
101
|
+
module Persistence
|
102
|
+
include PersistenceMethods
|
103
|
+
module_function :create_record, :update_record, :destroy_record
|
104
|
+
end
|
105
|
+
|
102
106
|
protected
|
103
107
|
|
104
108
|
def protocol_handler_class = ProtocolHandler
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveCypher
|
4
|
+
module ConnectionAdapters
|
5
|
+
# Common persistence helpers shared by adapters
|
6
|
+
module PersistenceMethods
|
7
|
+
# Create a record in the database and update model state.
|
8
|
+
# @param model [ActiveCypher::Base, ActiveCypher::Relationship] the model instance
|
9
|
+
# @return [Boolean] true if created successfully
|
10
|
+
def create_record(model)
|
11
|
+
props = model.send(:attributes_for_persistence)
|
12
|
+
labels = if model.class.respond_to?(:labels)
|
13
|
+
model.class.labels
|
14
|
+
else
|
15
|
+
[model.class.label_name.to_s]
|
16
|
+
end
|
17
|
+
label_string = labels.map { |l| ":#{l}" }.join
|
18
|
+
|
19
|
+
cypher = "CREATE (n#{label_string} $props) RETURN id(n) AS internal_id"
|
20
|
+
data = model.connection.execute_cypher(cypher, { props: props }, 'Create')
|
21
|
+
|
22
|
+
return false if data.blank? || !data.first.key?(:internal_id)
|
23
|
+
|
24
|
+
model.internal_id = data.first[:internal_id]
|
25
|
+
model.instance_variable_set(:@new_record, false)
|
26
|
+
model.send(:changes_applied)
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
# Update a record in the database based on model changes.
|
31
|
+
# @param model [ActiveCypher::Base, ActiveCypher::Relationship] the model instance
|
32
|
+
# @return [Boolean] true if update succeeded
|
33
|
+
def update_record(model)
|
34
|
+
changes = model.send(:changes_to_save)
|
35
|
+
return true if changes.empty?
|
36
|
+
|
37
|
+
labels = if model.class.respond_to?(:labels)
|
38
|
+
model.class.labels
|
39
|
+
else
|
40
|
+
[model.class.label_name.to_s]
|
41
|
+
end
|
42
|
+
|
43
|
+
label_string = labels.map { |l| ":#{l}" }.join
|
44
|
+
set_clauses = changes.keys.map { |property| "n.#{property} = $#{property}" }.join(', ')
|
45
|
+
params = changes.merge(node_id: model.internal_id)
|
46
|
+
|
47
|
+
cypher = "MATCH (n#{label_string}) WHERE id(n) = $node_id SET #{set_clauses} RETURN n"
|
48
|
+
model.connection.execute_cypher(cypher, params, 'Update')
|
49
|
+
|
50
|
+
model.send(:changes_applied)
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
# Destroy a record in the database.
|
55
|
+
# @param model [ActiveCypher::Base, ActiveCypher::Relationship] the model instance
|
56
|
+
# @return [Boolean] true if a record was deleted
|
57
|
+
def destroy_record(model)
|
58
|
+
labels = if model.class.respond_to?(:labels)
|
59
|
+
model.class.labels
|
60
|
+
else
|
61
|
+
[model.class.label_name]
|
62
|
+
end
|
63
|
+
label_string = labels.map { |l| ":#{l}" }.join
|
64
|
+
|
65
|
+
cypher = <<~CYPHER
|
66
|
+
MATCH (n#{label_string})
|
67
|
+
WHERE id(n) = $node_id
|
68
|
+
DETACH DELETE n
|
69
|
+
RETURN count(*) AS deleted
|
70
|
+
CYPHER
|
71
|
+
|
72
|
+
result = model.connection.execute_cypher(cypher, { node_id: model.internal_id }, 'Destroy')
|
73
|
+
result.present? && result.first[:deleted].to_i.positive?
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -20,20 +20,10 @@ module ActiveCypher
|
|
20
20
|
raise 'Cannot destroy a new record' if new_record?
|
21
21
|
raise 'Record already destroyed' if destroyed?
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
label_string = labels.map { |l| ":#{l}" }.join
|
23
|
+
adapter = adapter_class
|
24
|
+
raise NotImplementedError, "#{adapter} does not implement Persistence" unless adapter&.const_defined?(:Persistence)
|
26
25
|
|
27
|
-
|
28
|
-
MATCH (#{n}#{label_string})
|
29
|
-
WHERE id(#{n}) = #{internal_id}
|
30
|
-
DETACH DELETE #{n}
|
31
|
-
RETURN count(*) AS deleted
|
32
|
-
CYPHER
|
33
|
-
|
34
|
-
result = self.class.connection.execute_cypher(cypher, {}, 'Destroy')
|
35
|
-
|
36
|
-
if result.present? && result.first[:deleted].to_i.positive?
|
26
|
+
if adapter::Persistence.destroy_record(self)
|
37
27
|
@destroyed = true
|
38
28
|
freeze
|
39
29
|
true
|
@@ -121,40 +121,10 @@ module ActiveCypher
|
|
121
121
|
# Because nothing says "production ready" like a hand-crafted query.
|
122
122
|
# If this method ever works on the first try, that's not engineering—it's back magick.
|
123
123
|
def create_record
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
labels = self.class.respond_to?(:labels) ? self.class.labels : [self.class.label_name.to_s]
|
129
|
-
|
130
|
-
# For Memgraph, construct direct Cypher query
|
131
|
-
label_string = labels.map { |l| ":#{l}" }.join
|
132
|
-
|
133
|
-
# Handle properties for Cypher query
|
134
|
-
props_str = props.map do |k, v|
|
135
|
-
value_str = if v.nil?
|
136
|
-
'NULL'
|
137
|
-
elsif v.is_a?(String)
|
138
|
-
"'#{v.gsub("'", "\\\\'")}'"
|
139
|
-
elsif v.is_a?(Numeric) || v.is_a?(TrueClass) || v.is_a?(FalseClass)
|
140
|
-
v.to_s
|
141
|
-
else
|
142
|
-
"'#{v.to_s.gsub("'", "\\\\'")}'"
|
143
|
-
end
|
144
|
-
"#{k}: #{value_str}"
|
145
|
-
end.join(', ')
|
146
|
-
|
147
|
-
cypher = "CREATE (#{n}#{label_string} {#{props_str}}) " \
|
148
|
-
"RETURN id(#{n}) AS internal_id"
|
149
|
-
|
150
|
-
data = self.class.connection.execute_cypher(cypher, {}, 'Create')
|
151
|
-
|
152
|
-
return false if data.blank? || !data.first.key?(:internal_id)
|
153
|
-
|
154
|
-
self.internal_id = data.first[:internal_id]
|
155
|
-
@new_record = false
|
156
|
-
changes_applied
|
157
|
-
true
|
124
|
+
adapter = adapter_class
|
125
|
+
raise NotImplementedError, "#{adapter} does not implement Persistence" unless adapter&.const_defined?(:Persistence)
|
126
|
+
|
127
|
+
adapter::Persistence.create_record(self)
|
158
128
|
end
|
159
129
|
|
160
130
|
# Returns a hash of attributes that have changed and their spicy new values.
|
@@ -173,35 +143,10 @@ module ActiveCypher
|
|
173
143
|
#
|
174
144
|
# @return [Boolean] true if we updated something, or just acted like we did.
|
175
145
|
def update_record
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
# Use all labels for database operations
|
180
|
-
labels = self.class.respond_to?(:labels) ? self.class.labels : [self.class.label_name]
|
181
|
-
|
182
|
-
label_string = labels.map { |l| ":#{l}" }.join
|
183
|
-
set_clauses = changes.map do |property, value|
|
184
|
-
# Handle different value types appropriately
|
185
|
-
if value.nil?
|
186
|
-
"n.#{property} = NULL"
|
187
|
-
elsif value.is_a?(String)
|
188
|
-
"n.#{property} = '#{value.gsub("'", "\\\\'")}'"
|
189
|
-
elsif value.is_a?(Numeric) || value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
190
|
-
"n.#{property} = #{value}"
|
191
|
-
else
|
192
|
-
"n.#{property} = '#{value.to_s.gsub("'", "\\\\'")}'"
|
193
|
-
end
|
194
|
-
end.join(', ')
|
195
|
-
|
196
|
-
cypher = "MATCH (n#{label_string}) " \
|
197
|
-
"WHERE id(n) = #{internal_id} " \
|
198
|
-
"SET #{set_clauses} " \
|
199
|
-
'RETURN n'
|
200
|
-
|
201
|
-
self.class.connection.execute_cypher(cypher, {}, 'Update')
|
146
|
+
adapter = adapter_class
|
147
|
+
raise NotImplementedError, "#{adapter} does not implement Persistence" unless adapter&.const_defined?(:Persistence)
|
202
148
|
|
203
|
-
|
204
|
-
true
|
149
|
+
adapter::Persistence.update_record(self)
|
205
150
|
end
|
206
151
|
end
|
207
152
|
end
|
@@ -29,6 +29,9 @@ require 'active_support/core_ext/hash/indifferent_access'
|
|
29
29
|
|
30
30
|
module ActiveCypher
|
31
31
|
class Relationship
|
32
|
+
# Define connects_to_mappings as a class attribute to match ActiveCypher::Base
|
33
|
+
class_attribute :connects_to_mappings, default: {}
|
34
|
+
|
32
35
|
# --------------------------------------------------------------
|
33
36
|
# Mix‑ins
|
34
37
|
# --------------------------------------------------------------
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'fileutils'
|
2
4
|
require 'optparse'
|
3
5
|
|
@@ -5,7 +7,7 @@ module ActiveCypher
|
|
5
7
|
module Schema
|
6
8
|
# Dumps the graph schema to a Cypher script
|
7
9
|
class Dumper
|
8
|
-
DEFAULT_PATH = 'graphdb'
|
10
|
+
DEFAULT_PATH = 'graphdb'
|
9
11
|
|
10
12
|
def initialize(connection = ActiveCypher::Base.connection, base_dir: Dir.pwd)
|
11
13
|
@connection = connection
|
@@ -60,7 +62,7 @@ module ActiveCypher
|
|
60
62
|
def catalog_from_migrations
|
61
63
|
idx = []
|
62
64
|
cons = []
|
63
|
-
Dir[File.join(@base_dir, 'graphdb', 'migrate', '*.rb')].
|
65
|
+
Dir[File.join(@base_dir, 'graphdb', 'migrate', '*.rb')].each do |file|
|
64
66
|
require file
|
65
67
|
class_name = File.basename(file, '.rb').split('_', 2).last.camelize
|
66
68
|
klass = Object.const_get(class_name)
|
data/lib/cyrel/pattern/node.rb
CHANGED
@@ -7,7 +7,7 @@ module Cyrel
|
|
7
7
|
module Pattern
|
8
8
|
class Node
|
9
9
|
include ActiveModel::Model
|
10
|
-
include ActiveModel::Attributes
|
10
|
+
include ActiveModel::Attributes
|
11
11
|
include Cyrel::Parameterizable
|
12
12
|
|
13
13
|
attribute :alias_name, Cyrel::Types::SymbolType.new
|
@@ -1,16 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
namespace :graphdb do
|
4
|
+
# bin/rails graphdb:migrate
|
4
5
|
desc 'Run graph database migrations'
|
5
6
|
task migrate: :environment do
|
6
7
|
ActiveCypher::Migrator.new.migrate!
|
7
8
|
puts 'GraphDB migrations complete'
|
8
9
|
end
|
9
10
|
|
11
|
+
# bin/rails graphdb:status
|
10
12
|
desc 'Show graph database migration status'
|
11
13
|
task status: :environment do
|
12
14
|
ActiveCypher::Migrator.new.status.each do |m|
|
13
|
-
puts format('%-
|
15
|
+
puts format('%-4<status>s %<version>s %<name>s', m)
|
14
16
|
end
|
15
17
|
end
|
16
18
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activecypher
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.
|
4
|
+
version: 0.7.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Abdelkader Boudih
|
@@ -119,6 +119,7 @@ files:
|
|
119
119
|
- lib/active_cypher/connection_adapters/abstract_bolt_adapter.rb
|
120
120
|
- lib/active_cypher/connection_adapters/memgraph_adapter.rb
|
121
121
|
- lib/active_cypher/connection_adapters/neo4j_adapter.rb
|
122
|
+
- lib/active_cypher/connection_adapters/persistence_methods.rb
|
122
123
|
- lib/active_cypher/connection_adapters/registry.rb
|
123
124
|
- lib/active_cypher/connection_handler.rb
|
124
125
|
- lib/active_cypher/connection_pool.rb
|
@@ -213,6 +214,7 @@ files:
|
|
213
214
|
- lib/cyrel/types/hash_type.rb
|
214
215
|
- lib/cyrel/types/symbol_type.rb
|
215
216
|
- lib/tasks/graphdb_migrate.rake
|
217
|
+
- lib/tasks/graphdb_schema.rake
|
216
218
|
- sig/activecypher.rbs
|
217
219
|
homepage: https://github.com/seuros/activecypher
|
218
220
|
licenses:
|