activecypher 0.7.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5ccd00478564eeb903b76a423867e21973268d6c23af232de7329abe94263d5
4
- data.tar.gz: a6e74f587ad7c543123840d5f44440efca909a44efa0a59be5b8ce15d8d39f0f
3
+ metadata.gz: 738573111aeb926ef359c2bd2643e07d25f83b76d63f7cebf0d31e762c06fabf
4
+ data.tar.gz: 8ede3884325f7e86e4ca06dea636e0193d8742fc19b0ed2e92724f3270f31f7f
5
5
  SHA512:
6
- metadata.gz: 1b90a82d07ffc1504ddf9d07fcd88a80d3371d32ed09eb8f799c4d228958e090522a44cac2abd6bdf661983fda43ce95b898fe440f8e76c0427621f55ad342a0
7
- data.tar.gz: bd848d6485fdc502673f88ad3ea3dd776d486a1e11cf501592994efba1347cb799b28af0b6f06e5a29daf3a60c79c39072181fb03d36f0938df5ed52699bd881
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,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_cypher/schema/catalog'
4
-
5
3
  module ActiveCypher
6
4
  module ConnectionAdapters
7
5
  class MemgraphAdapter < AbstractBoltAdapter
@@ -46,7 +44,26 @@ module ActiveCypher
46
44
  self.class
47
45
  end
48
46
 
49
- # Memgraph defaults to **implicit auto‑commit** transactions :contentReference[oaicite:1]{index=1},
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
50
67
  # so we simply run the Cypher and return the rows.
51
68
  def execute_cypher(cypher, params = {}, ctx = 'Query')
52
69
  rows = run(cypher.gsub(/\belementId\(/i, 'id('), params, context: ctx)
@@ -122,6 +139,11 @@ module ActiveCypher
122
139
  end
123
140
  end
124
141
 
142
+ module Persistence
143
+ include PersistenceMethods
144
+ module_function :create_record, :update_record, :destroy_record
145
+ end
146
+
125
147
  class ProtocolHandler < AbstractProtocolHandler
126
148
  def extract_version(agent)
127
149
  agent[%r{Memgraph/([\d.]+)}, 1] || 'unknown'
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_cypher/schema/catalog'
4
-
5
3
  module ActiveCypher
6
4
  module ConnectionAdapters
7
5
  class Neo4jAdapter < AbstractBoltAdapter
@@ -100,6 +98,11 @@ module ActiveCypher
100
98
  metadata.compact
101
99
  end
102
100
 
101
+ module Persistence
102
+ include PersistenceMethods
103
+ module_function :create_record, :update_record, :destroy_record
104
+ end
105
+
103
106
  protected
104
107
 
105
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
- n = :n
24
- labels = self.class.respond_to?(:labels) ? self.class.labels : [self.class.label_name]
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
- cypher = <<~CYPHER
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
- props = attributes_for_persistence
125
- n = :n
126
-
127
- # Use all labels for database operations
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
- changes = changes_to_save
177
- return true if changes.empty?
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
- changes_applied
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,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveCypher
4
- VERSION = '0.7.1'
4
+ VERSION = '0.7.2'
5
5
 
6
6
  def self.gem_version
7
7
  Gem::Version.new VERSION
@@ -7,7 +7,7 @@ module Cyrel
7
7
  module Pattern
8
8
  class Node
9
9
  include ActiveModel::Model
10
- include ActiveModel::Attributes # :contentReference[oaicite:3]{index=3}
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('%-4s %s %s', m[:status], m[:version], m[:name])
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.1
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