activecypher 0.14.1 → 0.15.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfba386d94660c96cf6078ac0a7d370d2dd5670e9316a3de2ee917f409f227ab
4
- data.tar.gz: e2e70d29dc99279b51f810363d50749e470556481e80c8c8276fc80ba612b7d4
3
+ metadata.gz: cb2fe676154a99ad8fc54cfe88731a1c7ce4759235e5103e0d87662c4cd450c2
4
+ data.tar.gz: 8982b47d70418c6fcc2ec6e4e8a4bd9513df9ac94ab9c8d14b19f0b9af699c22
5
5
  SHA512:
6
- metadata.gz: 1fbe83583d2cdb4a7ea898e8ce5876cfd6907af332ffd7b77643473026ac5fd0d3714bcf037876d2c8ebd8722594a9e5635ae0b3c816c4a98ed9f49dad99e523
7
- data.tar.gz: bc13b5392ce91d0247bd4166310e8967676763b9d11f3156206f7a8e3b004f3158b5341dfbadfec82677fc887ef6aedfc8fe51081ea722d847cce60eb155f1d0
6
+ metadata.gz: 1d227c0f135e4203eee987c763b3d6bb2e61e4b24fbc3a5737166bbe1288d042d42d5cc2c83172f2df6d68f42480f76e8260a4571eee533cb53ce68c93f73228
7
+ data.tar.gz: 2d47c7787d443326b89d8f734107f4492a2cc34f7930ae0c46fbd9e7fda3d1249ea0840376d3653218872ece6434151a557db7b430167a690824981abe54f523
@@ -37,7 +37,10 @@ module ActiveCypher
37
37
  load_target unless @records
38
38
  @records.each(&)
39
39
  end
40
- alias to_a each
40
+
41
+ def to_a
42
+ each.to_a
43
+ end
41
44
 
42
45
  # Returns the size, because counting things is the only certainty in life.
43
46
  #
@@ -114,14 +114,20 @@ module ActiveCypher
114
114
 
115
115
  # Resolve the target node class
116
116
  target_class = target_class_name.constantize
117
- a_alias = :start
118
- b_alias = :target
119
117
 
120
- # Pattern nodes (immutable)
121
- a_node = Cyrel::Pattern::Node.new(a_alias, labels: self.class.label_name)
122
- b_node = Cyrel::Pattern::Node.new(b_alias, labels: target_class.label_name)
118
+ owner_alias = :start
119
+ related_alias = :target
120
+
121
+ # owner = the node we're querying from (self)
122
+ # related = the node we want to fetch
123
+ owner_node = Cyrel::Pattern::Node.new(owner_alias, labels: self.class.label_name)
124
+ related_node = Cyrel::Pattern::Node.new(related_alias, labels: target_class.label_name)
123
125
 
124
- # Relationship pattern with correct direction
126
+ # The relationship token renders the arrow direction itself:
127
+ # :out → -[:TYPE]-> e.g. (start:Person)-[:ENJOYS]->(target:Activity)
128
+ # :in → <-[:TYPE]- e.g. (start:Activity)<-[:ENJOYS]-(target:Person)
129
+ # :both → -[:TYPE]- e.g. (start)-[:TYPE]-(target)
130
+ # Node order is always [owner, rel, related] — never swapped.
125
131
  rel_direction = case direction
126
132
  when :out then Cyrel::Direction::OUT
127
133
  when :in then Cyrel::Direction::IN
@@ -133,17 +139,13 @@ module ActiveCypher
133
139
  direction: rel_direction
134
140
  )
135
141
 
136
- # Build undirected / outgoing / incoming path
137
- path = case direction
138
- when :in then Cyrel::Pattern::Path.new([b_node, rel_node, a_node])
139
- else Cyrel::Pattern::Path.new([a_node, rel_node, b_node])
140
- end
142
+ path = Cyrel::Pattern::Path.new([owner_node, rel_node, related_node])
141
143
 
142
144
  # Compose query MATCH – WHERE – RETURN
143
145
  query = Cyrel::Query.new
144
146
  .match(path)
145
- .where(Cyrel.node_id(a_alias).eq(internal_id))
146
- .return_(b_alias)
147
+ .where(Cyrel.node_id(owner_alias).eq(internal_id))
148
+ .return_(related_alias)
147
149
 
148
150
  base_relation = Relation.new(target_class, query)
149
151
 
@@ -13,7 +13,7 @@ module ActiveCypher
13
13
  class Base
14
14
  # @!attribute [rw] connects_to_mappings
15
15
  # @return [Hash] Because every base class needs a mapping it will never use directly.
16
- class_attribute :connects_to_mappings, default: {}
16
+ class_attribute :connects_to_mappings, default: { reading: :primary, writing: :primary }
17
17
 
18
18
  # Rails/ActiveModel foundations
19
19
  include Logging
@@ -30,6 +30,7 @@ module ActiveCypher
30
30
  include Model::Querying
31
31
  include Model::Abstract
32
32
  include Model::Attributes
33
+ include Model::ConnectionHandling
33
34
  include Model::ConnectionOwner
34
35
  include Model::Persistence
35
36
  include Model::Destruction
@@ -44,7 +45,7 @@ module ActiveCypher
44
45
  # Determine the current role (e.g., :writing, :reading)
45
46
  # ActiveCypher::RuntimeRegistry.current_role defaults to :writing
46
47
  # Only use db_key for pool lookup
47
- mapping = connects_to_mappings if respond_to?(:connects_to_mappings)
48
+ mapping = connects_to_mappings
48
49
  role = ActiveCypher::RuntimeRegistry.current_role || :writing
49
50
  # Debug guardrails removed in release code; rely on role/shard registry.
50
51
 
@@ -81,6 +82,17 @@ module ActiveCypher
81
82
  # Wrap it all up in a fake-sane object string, so you can pretend your data is organized.
82
83
  "#<#{self.class} #{parts.join(', ')}>"
83
84
  end
85
+
86
+ def internal_id
87
+ working_id = super
88
+ return working_id if working_id.nil?
89
+
90
+ if connection.respond_to?(:id_type_conversion)
91
+ connection.id_type_conversion(working_id)
92
+ else
93
+ working_id
94
+ end
95
+ end
84
96
 
85
97
  # Because Rails needs to feel included, too.
86
98
  ActiveSupport.run_load_hooks(:active_cypher, self)
@@ -63,6 +63,10 @@ module ActiveCypher
63
63
  self.class
64
64
  end
65
65
 
66
+ def id_type_conversion(incoming)
67
+ return incoming.to_i
68
+ end
69
+
66
70
  # Memgraph uses different constraint syntax than Neo4j
67
71
  def ensure_schema_migration_constraint
68
72
  execute_ddl(<<~CYPHER)
@@ -7,7 +7,6 @@ module ActiveCypher
7
7
  module ConnectionOwner
8
8
  extend ActiveSupport::Concern
9
9
  include ActiveCypher::Logging
10
- include ActiveCypher::Model::ConnectionHandling
11
10
 
12
11
  def self.db_key_for(mapping, role)
13
12
  return nil unless mapping.is_a?(Hash)
@@ -66,7 +65,7 @@ module ActiveCypher
66
65
  # Always dynamically fetch the connection for the current db_key
67
66
  def connection
68
67
  handler = connection_handler
69
- mapping = connects_to_mappings if respond_to?(:connects_to_mappings)
68
+ mapping = connects_to_mappings
70
69
  role = ActiveCypher::RuntimeRegistry.current_role || :writing
71
70
  db_key = ConnectionOwner.db_key_for(mapping, role)
72
71
  db_key = db_key.to_sym if db_key.respond_to?(:to_sym)
@@ -36,6 +36,16 @@ module ActiveCypher
36
36
  clear_changes_information
37
37
  end
38
38
  end
39
+
40
+ def ==(other)
41
+ # Compares by class and internal graph id only.
42
+ # Note: an unsaved modification will still compare equal to the persisted version.
43
+ return false unless other.instance_of?(self.class)
44
+
45
+ internal_id == other.internal_id
46
+ end
47
+
48
+
39
49
  end
40
50
  end
41
51
  end
@@ -44,8 +44,8 @@ module ActiveCypher
44
44
  # Find all abstract node base classes with connects_to mappings
45
45
  ObjectSpace.each_object(Class) do |klass|
46
46
  next unless klass < ActiveCypher::Base
47
- next unless klass.respond_to?(:abstract_class?) && klass.abstract_class?
48
- next unless klass.respond_to?(:connects_to_mappings) && klass.connects_to_mappings.present?
47
+ next unless klass.abstract_class?
48
+ next if klass.connects_to_mappings.empty?
49
49
 
50
50
  # Register pools for each role in connects_to mapping
51
51
  klass.connects_to_mappings.each_value do |conn_name|
@@ -164,8 +164,8 @@ module ActiveCypher
164
164
  # 1. Pull out the node payload and the elementId string
165
165
  # ------------------------------------------------------------
166
166
  if row.is_a?(Hash)
167
- node_payload = row[:n] || row['n'] || row
168
- element_id = row[:internal_id] || row['internal_id']
167
+ node_payload = row[:n] || row['n'] || row[:target] || row['target'] || row
168
+ element_id = row[:internal_id] || row['internal_id'] || node_payload&.dig(1, 0)
169
169
  else # Array row: [node, id]
170
170
  node_payload, element_id = row
171
171
  end
@@ -176,6 +176,7 @@ module ActiveCypher
176
176
  # ------------------------------------------------------------
177
177
  if node_payload.is_a?(Array) && node_payload.first == 78
178
178
  # Re‑use the adapter's private helper for consistency
179
+ # why is it private? This seems to be the only place it's called
179
180
  node_payload = model_class.connection
180
181
  .send(:process_node, node_payload)
181
182
  end
@@ -29,9 +29,6 @@ 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
-
35
32
  # --------------------------------------------------------------
36
33
  # Mix‑ins
37
34
  # --------------------------------------------------------------
@@ -44,7 +41,6 @@ module ActiveCypher
44
41
  include Model::ConnectionOwner
45
42
  include Logging
46
43
  include Model::Abstract
47
- include Model::ConnectionHandling
48
44
  include Model::Callbacks
49
45
  include Model::Countable
50
46
 
@@ -203,22 +199,24 @@ module ActiveCypher
203
199
 
204
200
  rel_type = relationship_type
205
201
 
202
+ id_func = connection.class::ID_FUNCTION
203
+
206
204
  # Build WHERE conditions for the attributes
207
205
  conditions = []
208
206
  params = {}
209
207
 
210
- attributes.each_with_index do |(key, value), index|
211
- param_name = :"p#{index + 1}"
212
- conditions << "r.#{key} = $#{param_name}"
213
- params[param_name] = value
208
+ if attributes.key?(:internal_id)
209
+ where_clause = "#{id_func}(r) = $p1"
210
+ params['p1'] = attributes[:internal_id]
211
+ else
212
+ attributes.each_with_index do |(key, value), index|
213
+ param_name = :"p#{index + 1}"
214
+ conditions << "r.#{key} = $#{param_name}"
215
+ params[param_name] = value
216
+ end
217
+ where_clause = conditions.join(' AND ')
214
218
  end
215
219
 
216
- where_clause = conditions.join(' AND ')
217
-
218
- # Determine ID function based on adapter type
219
- adapter_class = connection.class
220
- id_func = adapter_class.const_defined?(:ID_FUNCTION) ? adapter_class::ID_FUNCTION : 'id'
221
-
222
220
  cypher = <<~CYPHER
223
221
  MATCH ()-[r:#{rel_type}]-()
224
222
  WHERE #{where_clause}
@@ -234,6 +232,12 @@ module ActiveCypher
234
232
  # Extract relationship data and instantiate
235
233
  rel_data = row[:r] || row['r']
236
234
  rid = row[:rid] || row['rid']
235
+ from_node_id = (row[:from_node] || row['from_node'])&.dig(1, 0)
236
+ to_node_id = (row[:to_node] || row['to_node'])&.dig(1, 0)
237
+
238
+ # this is extra queries, but easier than navigating instantiation from the row data
239
+ from_node = Object.const_get(self.from_class).find(from_node_id)
240
+ to_node = Object.const_get(self.to_class).find(to_node_id)
237
241
 
238
242
  # Extract properties from the relationship data
239
243
  # Memgraph returns relationships wrapped as [type_code, [actual_data]]
@@ -256,7 +260,7 @@ module ActiveCypher
256
260
  attrs = attrs.transform_keys(&:to_sym)
257
261
  attrs[:internal_id] = rid if rid
258
262
 
259
- instantiate(attrs)
263
+ instantiate(attrs, from_node: from_node, to_node: to_node)
260
264
  end
261
265
 
262
266
  # Find the first relationship or raise an exception
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveCypher
4
- VERSION = '0.14.1'
4
+ VERSION = '0.15.1'
5
5
 
6
6
  def self.gem_version
7
7
  Gem::Version.new VERSION
@@ -8,11 +8,13 @@ namespace :graphdb do
8
8
  puts 'GraphDB migrations complete'
9
9
  end
10
10
 
11
- # bin/rails graphdb:status
12
- desc 'Show graph database migration status'
13
- task status: :environment do
14
- ActiveCypher::Migrator.new.status.each do |m|
15
- puts format('%-4<status>s %<version>s %<name>s', m)
11
+ namespace :migrate do
12
+ # bin/rails graphdb:migrate:status
13
+ desc 'Show graph database migration status'
14
+ task status: :environment do
15
+ ActiveCypher::Migrator.new.status.each do |m|
16
+ puts format('%-4<status>s %<version>s %<name>s', m)
17
+ end
16
18
  end
17
19
  end
18
20
  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.14.1
4
+ version: 0.15.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -293,7 +293,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
293
293
  - !ruby/object:Gem::Version
294
294
  version: '0'
295
295
  requirements: []
296
- rubygems_version: 3.6.9
296
+ rubygems_version: 4.0.6
297
297
  specification_version: 4
298
298
  summary: OpenCypher Adapter ala ActiveRecord
299
299
  test_files: []