activecypher 0.14.2 → 0.15.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: 4c164d949a4eedae68ef59d998e8957201ac57e7710ebfc5a599e82254506262
4
- data.tar.gz: 6e318063e49efaad59dcee182e1be1f2f6992a296144f0b3c96fd296b6d2a6a5
3
+ metadata.gz: dc6fb1ecc443117671c36c2b287b44d0059d706c8543ef113915fe0c67e8063d
4
+ data.tar.gz: 7f7a6e862f2c146dc19e89827e2b0ab2333bcc58f801b07a78e3e0c89f17fc6f
5
5
  SHA512:
6
- metadata.gz: 560dfa5c4ead5c8d9e8ac6a14925859bd684bc13ec7b1e9e5d42e010240d23dee11985b8aeecbc419fdcca9911671f08d939486a641b0e1ff8cd2517b5192750
7
- data.tar.gz: 33c472bdab0d4b5011c82431a12fe263002043b7337550fe89babb1f09388ca9afb772a4ff7a1eeb2081000d1b2a8dcb61faa80fb3fda4720574bde006071bc0
6
+ metadata.gz: 6cd38267fc15f40e53c9d9d79d11fe52138cc20aa93290043e2aae7834616b6d78afb952a7c1c1ad6baf8e08531170618e43f297d70074c9db806928cd89b807
7
+ data.tar.gz: c2b0186231f35c838a56272ff6222f64af8a7be53891c8ed90260e297acdccef42992f28b65729ff61410172570cdcd92209c3c817d920d30341cc88906be9ff
@@ -37,12 +37,15 @@ 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
  #
44
47
  # @return [Integer] The number of records in the collection
45
- def size = load_target.size
48
+ def size = load_target.size
46
49
  alias length size
47
50
 
48
51
  # Fully refresh from the database.
@@ -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
 
@@ -283,6 +285,12 @@ module ActiveCypher
283
285
  instance_variable_set(instance_var, associate)
284
286
  end
285
287
 
288
+ define_build_and_create_methods(name, target_class_name)
289
+ end
290
+
291
+ # Defines build_<name> and create_<name> for singular associations
292
+ # (shared by belongs_to and has_one).
293
+ def define_build_and_create_methods(name, target_class_name)
286
294
  # Define build method (e.g., build_author(name: "New Author"))
287
295
  define_method("build_#{name}") do |attributes = {}|
288
296
  target_class = target_class_name.constantize
@@ -429,25 +437,7 @@ module ActiveCypher
429
437
  instance_variable_set(instance_var, associate)
430
438
  end
431
439
 
432
- # Define build method (e.g., build_profile(data: {...}))
433
- define_method("build_#{name}") do |attributes = {}|
434
- target_class = target_class_name.constantize
435
- # TODO: Potentially set the inverse association reference here
436
- # For now, just instantiate the target class
437
- target_class.new(attributes)
438
- end
439
-
440
- # Define create method (e.g., create_profile(data: {...}))
441
- define_method("create_#{name}") do |attributes = {}|
442
- # Build the instance
443
- instance = public_send("build_#{name}", attributes)
444
- # Save the instance
445
- instance.save
446
- # If save is successful, associate it using the = method
447
- public_send("#{name}=", instance) if instance.persisted?
448
- # Return the instance
449
- instance
450
- end
440
+ define_build_and_create_methods(name, target_class_name)
451
441
  end
452
442
  end
453
443
 
@@ -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
 
@@ -82,6 +83,17 @@ module ActiveCypher
82
83
  "#<#{self.class} #{parts.join(', ')}>"
83
84
  end
84
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
96
+
85
97
  # Because Rails needs to feel included, too.
86
98
  ActiveSupport.run_load_hooks(:active_cypher, self)
87
99
  end
@@ -485,7 +485,6 @@ module ActiveCypher
485
485
  session(database: db).write_transaction(db: db, timeout: timeout, metadata: metadata, &)
486
486
  end
487
487
 
488
-
489
488
  # ────────────────────────────────────────────────────────────────────
490
489
  # HEALTH AND VERSION DETECTION METHODS
491
490
  # ────────────────────────────────────────────────────────────────────
@@ -514,21 +513,17 @@ module ActiveCypher
514
513
  def health_check
515
514
  return { healthy: false, response_time_ms: nil, details: 'Not connected' } unless connected?
516
515
 
517
- result = nil
516
+ nil
518
517
 
519
518
  begin
520
- result = Sync do
521
- case database_type
522
- when :neo4j
523
- perform_neo4j_health_check
524
- when :memgraph
525
- perform_memgraph_health_check
526
- else
527
- perform_generic_health_check
528
- end
519
+ Sync do
520
+ query = case database_type
521
+ when :neo4j then 'RETURN 1 AS result'
522
+ when :memgraph then 'SHOW STORAGE INFO'
523
+ else 'RETURN 1'
524
+ end
525
+ perform_health_check_query(query)
529
526
  end
530
-
531
- result
532
527
  rescue ConnectionError, ProtocolError => e
533
528
  { healthy: false, response_time_ms: nil, details: "Health check failed: #{e.message}" }
534
529
  end
@@ -707,96 +702,15 @@ module ActiveCypher
707
702
  }
708
703
  end
709
704
 
710
- # Performs Neo4j-specific health check using RETURN 1 (fallback from db.ping).
711
- #
712
- # @return [Hash] health check result
713
- def perform_neo4j_health_check
714
- start_time = Time.now
715
-
716
- begin
717
- write_message(Messaging::Run.new('RETURN 1 AS result', {}, {}), 'HEALTH_CHECK')
718
- run_response = read_message
719
-
720
- case run_response
721
- when Messaging::Success
722
- # Send PULL message to complete the transaction
723
- write_message(Messaging::Pull.new({ 'n' => -1 }), 'PULL')
724
-
725
- # Read messages until we get SUCCESS or FAILURE
726
- response_time = nil
727
- loop do
728
- msg = read_message
729
- case msg
730
- when Messaging::Record
731
- # Skip records, we just want to know if the query succeeded
732
- next
733
- when Messaging::Success
734
- response_time = ((Time.now - start_time) * 1000).round(2)
735
- return { healthy: true, response_time_ms: response_time, details: 'RETURN 1 succeeded' }
736
- when Messaging::Failure
737
- return { healthy: false, response_time_ms: nil, details: 'RETURN 1 failed with error' }
738
- else
739
- return { healthy: false, response_time_ms: nil, details: 'RETURN 1 unexpected response' }
740
- end
741
- end
742
- else
743
- { healthy: false, response_time_ms: nil, details: 'RETURN 1 run failed' }
744
- end
745
- ensure
746
- # Reset connection state after health check
747
- reset!
748
- end
749
- end
750
-
751
- # Performs Memgraph-specific health check using SHOW STORAGE INFO.
752
- #
753
- # @return [Hash] health check result
754
- def perform_memgraph_health_check
755
- start_time = Time.now
756
-
757
- begin
758
- write_message(Messaging::Run.new('SHOW STORAGE INFO', {}, {}), 'HEALTH_CHECK')
759
- run_response = read_message
760
-
761
- case run_response
762
- when Messaging::Success
763
- # Send PULL message to complete the transaction
764
- write_message(Messaging::Pull.new({ 'n' => -1 }), 'PULL')
765
-
766
- # Read messages until we get SUCCESS or FAILURE
767
- response_time = nil
768
- loop do
769
- msg = read_message
770
- case msg
771
- when Messaging::Record
772
- # Skip records, we just want to know if the query succeeded
773
- next
774
- when Messaging::Success
775
- response_time = ((Time.now - start_time) * 1000).round(2)
776
- return { healthy: true, response_time_ms: response_time, details: 'SHOW STORAGE INFO succeeded' }
777
- when Messaging::Failure
778
- return { healthy: false, response_time_ms: nil, details: 'SHOW STORAGE INFO failed with error' }
779
- else
780
- return { healthy: false, response_time_ms: nil, details: 'SHOW STORAGE INFO unexpected response' }
781
- end
782
- end
783
- else
784
- { healthy: false, response_time_ms: nil, details: 'SHOW STORAGE INFO run failed' }
785
- end
786
- ensure
787
- # Reset connection state after health check
788
- reset!
789
- end
790
- end
791
-
792
- # Performs generic health check using simple RETURN 1 query.
705
+ # Performs a health check by running the given query and draining the result.
793
706
  #
707
+ # @param query [String] database-appropriate health check query
794
708
  # @return [Hash] health check result
795
- def perform_generic_health_check
709
+ def perform_health_check_query(query)
796
710
  start_time = Time.now
797
711
 
798
712
  begin
799
- write_message(Messaging::Run.new('RETURN 1', {}, {}), 'HEALTH_CHECK')
713
+ write_message(Messaging::Run.new(query, {}, {}), 'HEALTH_CHECK')
800
714
  run_response = read_message
801
715
 
802
716
  case run_response
@@ -814,15 +728,15 @@ module ActiveCypher
814
728
  next
815
729
  when Messaging::Success
816
730
  response_time = ((Time.now - start_time) * 1000).round(2)
817
- return { healthy: true, response_time_ms: response_time, details: 'RETURN 1 succeeded' }
731
+ return { healthy: true, response_time_ms: response_time, details: "#{query} succeeded" }
818
732
  when Messaging::Failure
819
- return { healthy: false, response_time_ms: nil, details: 'RETURN 1 failed with error' }
733
+ return { healthy: false, response_time_ms: nil, details: "#{query} failed with error" }
820
734
  else
821
- return { healthy: false, response_time_ms: nil, details: 'RETURN 1 unexpected response' }
735
+ return { healthy: false, response_time_ms: nil, details: "#{query} unexpected response" }
822
736
  end
823
737
  end
824
738
  else
825
- { healthy: false, response_time_ms: nil, details: 'RETURN 1 run failed' }
739
+ { healthy: false, response_time_ms: nil, details: "#{query} run failed" }
826
740
  end
827
741
  ensure
828
742
  # Reset connection state after health check
@@ -40,9 +40,9 @@ module ActiveCypher
40
40
  #
41
41
  # @yieldparam session [Bolt::Session] The session to use
42
42
  # @return [Object] The result of the block
43
- def with_session(**kw, &block)
43
+ def with_session(**kw, &)
44
44
  Sync do
45
- _acquire_session(**kw, &block)
45
+ _acquire_session(**kw, &)
46
46
  end
47
47
  rescue Async::TimeoutError => e
48
48
  raise ActiveCypher::ConnectionError, "Connection pool timeout: #{e.message}"
@@ -55,11 +55,11 @@ module ActiveCypher
55
55
  #
56
56
  # @yieldparam session [Bolt::Session] The session to use
57
57
  # @return [Async::Task] A task that resolves to the block's result
58
- def async_with_session(**kw, &block)
58
+ def async_with_session(**kw, &)
59
59
  raise 'Cannot run async_with_session outside of an Async task' unless Async::Task.current?
60
60
 
61
61
  Async do
62
- _acquire_session(**kw, &block)
62
+ _acquire_session(**kw, &)
63
63
  end
64
64
  end
65
65
 
@@ -67,13 +67,12 @@ module ActiveCypher
67
67
  end
68
68
  end
69
69
 
70
- # The HELLO message. Because every protocol needs to start with a greeting before the disappointment.
71
- class Hello < Message
72
- SIGNATURE = 0x01
73
-
70
+ # Base for messages whose single field is a normalized metadata map.
71
+ # Subclasses only need to define their SIGNATURE.
72
+ class MetadataMessage < Message
74
73
  def initialize(metadata)
75
74
  meta = Messaging.normalize_map(metadata)
76
- super(SIGNATURE, [meta])
75
+ super(self.class::SIGNATURE, [meta])
77
76
  end
78
77
 
79
78
  def metadata
@@ -81,22 +80,27 @@ module ActiveCypher
81
80
  end
82
81
  end
83
82
 
84
- # The GOODBYE message. For when you've had enough of this session, or life.
85
- class Goodbye < Message
86
- SIGNATURE = 0x02
87
-
83
+ # Base for messages that carry no fields at all.
84
+ # Subclasses only need to define their SIGNATURE.
85
+ class EmptyMessage < Message
88
86
  def initialize
89
- super(SIGNATURE, [])
87
+ super(self.class::SIGNATURE, [])
90
88
  end
91
89
  end
92
90
 
91
+ # The HELLO message. Because every protocol needs to start with a greeting before the disappointment.
92
+ class Hello < MetadataMessage
93
+ SIGNATURE = 0x01
94
+ end
95
+
96
+ # The GOODBYE message. For when you've had enough of this session, or life.
97
+ class Goodbye < EmptyMessage
98
+ SIGNATURE = 0x02
99
+ end
100
+
93
101
  # The RESET message. Because sometimes you just want to pretend nothing ever happened.
94
- class Reset < Message
102
+ class Reset < EmptyMessage
95
103
  SIGNATURE = 0x0F
96
-
97
- def initialize
98
- super(SIGNATURE, [])
99
- end
100
104
  end
101
105
 
102
106
  # The RUN message. Because what else would you do with a database connection?
@@ -148,115 +152,52 @@ module ActiveCypher
148
152
  end
149
153
 
150
154
  # The COMMIT message. For when you want to pretend your changes are permanent.
151
- class Commit < Message
155
+ class Commit < EmptyMessage
152
156
  SIGNATURE = 0x12
153
-
154
- def initialize
155
- super(SIGNATURE, [])
156
- end
157
157
  end
158
158
 
159
159
  # The ROLLBACK message. Because sometimes you just want to undo your mistakes.
160
- class Rollback < Message
160
+ class Rollback < EmptyMessage
161
161
  SIGNATURE = 0x13
162
-
163
- def initialize
164
- super(SIGNATURE, [])
165
- end
166
162
  end
167
163
 
168
164
  # The DISCARD message. For when you want to throw away results, or your hopes.
169
- class Discard < Message
165
+ class Discard < MetadataMessage
170
166
  SIGNATURE = 0x2F
171
167
 
172
168
  # metadata: { n: <N>, qid: <QID> }, where n = -1 means all
173
- def initialize(metadata)
174
- meta = Messaging.normalize_map(metadata)
175
- super(SIGNATURE, [meta])
176
- end
177
-
178
- def metadata = fields.first
179
- def n = metadata[:n] || metadata['n']
180
- def qid = metadata[:qid] || metadata['qid']
169
+ def n = metadata[:n] || metadata['n']
170
+ def qid = metadata[:qid] || metadata['qid']
181
171
  end
182
172
 
183
173
  # The PULL message. Because sometimes you just want to see what you got.
184
- class Pull < Message
174
+ class Pull < MetadataMessage
185
175
  SIGNATURE = 0x3F
186
-
187
- def initialize(metadata)
188
- meta = Messaging.normalize_map(metadata)
189
- super(SIGNATURE, [meta])
190
- end
191
-
192
- def metadata
193
- fields.first
194
- end
195
176
  end
196
177
 
197
178
  # The ROUTE message. For when you want to pretend you have control over routing.
198
- class Route < Message
179
+ class Route < MetadataMessage
199
180
  SIGNATURE = 0x66
200
-
201
- def initialize(metadata)
202
- meta = Messaging.normalize_map(metadata)
203
- super(SIGNATURE, [meta])
204
- end
205
-
206
- def metadata
207
- fields.first
208
- end
209
181
  end
210
182
 
211
183
  # The LOGON message. Because authentication is just another chance to be rejected.
212
- class Logon < Message
184
+ class Logon < MetadataMessage
213
185
  SIGNATURE = 0x6A
214
-
215
- def initialize(metadata)
216
- meta = Messaging.normalize_map(metadata)
217
- super(SIGNATURE, [meta])
218
- end
219
-
220
- def metadata
221
- fields.first
222
- end
223
186
  end
224
187
 
225
188
  # The LOGOFF message. For when you want to leave quietly, without making a scene.
226
- class Logoff < Message
189
+ class Logoff < EmptyMessage
227
190
  SIGNATURE = 0x6B
228
-
229
- def initialize
230
- super(SIGNATURE, [])
231
- end
232
191
  end
233
192
 
234
193
  # The TELEMETRY message. Because someone, somewhere, cares about your metrics. Probably.
235
- class Telemetry < Message
194
+ class Telemetry < MetadataMessage
236
195
  SIGNATURE = 0x54
237
-
238
- def initialize(metadata)
239
- meta = Messaging.normalize_map(metadata)
240
- super(SIGNATURE, [meta])
241
- end
242
-
243
- def metadata
244
- fields.first
245
- end
246
196
  end
247
197
 
248
198
  # The SUCCESS message. The rarest of all Bolt messages.
249
- class Success < Message
199
+ class Success < MetadataMessage
250
200
  SIGNATURE = 0x70
251
-
252
- def initialize(metadata)
253
- meta = Messaging.normalize_map(metadata)
254
- super(SIGNATURE, [meta])
255
- end
256
-
257
- def metadata
258
- fields.first
259
- end
260
201
  end
261
202
 
262
203
  # The RECORD message. For when you actually get data back, against all odds.
@@ -273,27 +214,14 @@ module ActiveCypher
273
214
  end
274
215
 
275
216
  # The IGNORED message. For when the server just can't be bothered.
276
- class Ignored < Message
217
+ class Ignored < EmptyMessage
277
218
  SIGNATURE = 0x7E
278
-
279
- def initialize
280
- super(SIGNATURE, [])
281
- end
282
219
  end
283
220
 
284
221
  # The FAILURE message. The most honest message in the protocol.
285
- class Failure < Message
222
+ class Failure < MetadataMessage
286
223
  SIGNATURE = 0x7F
287
224
 
288
- def initialize(metadata)
289
- meta = Messaging.normalize_map(metadata)
290
- super(SIGNATURE, [meta])
291
- end
292
-
293
- def metadata
294
- fields.first
295
- end
296
-
297
225
  def code
298
226
  metadata['code']
299
227
  end
@@ -89,18 +89,7 @@ module ActiveCypher
89
89
  end
90
90
 
91
91
  def pack_map(map)
92
- size = map.size
93
-
94
- if size < 16 # TinyMap
95
- write_marker([TINY_MAP_MARKER_BASE | size].pack('C'))
96
- elsif size < 256 # MAP_8
97
- write_marker([MAP_8_MARKER, size].pack('CC'))
98
- elsif size < 65_536 # MAP_16
99
- write_marker([MAP_16_MARKER, size].pack('Cn'))
100
- else
101
- raise ProtocolError, "Map too large to pack (size: #{size})"
102
- # write_marker([MAP_32_MARKER, size].pack('CN>'))
103
- end
92
+ write_collection_marker(map.size, TINY_MAP_MARKER_BASE, MAP_8_MARKER, MAP_16_MARKER, 'Map')
104
93
 
105
94
  map.each do |key, value|
106
95
  pack(key.to_s) # Keys must be strings
@@ -109,21 +98,24 @@ module ActiveCypher
109
98
  end
110
99
 
111
100
  def pack_list(list)
112
- size = list.size
113
- if size < 16 # TinyList
114
- write_marker([TINY_LIST_MARKER_BASE | size].pack('C'))
115
- elsif size < 256 # LIST_8
116
- write_marker([LIST_8_MARKER, size].pack('CC'))
117
- elsif size < 65_536 # LIST_16
118
- write_marker([LIST_16_MARKER, size].pack('Cn')) # n is already network byte order
119
- else
120
- raise ProtocolError, "List too large to pack (size: #{size})"
121
- # write_marker([LIST_32_MARKER, size].pack('CN>')) # Use N> for network byte order
122
- end
101
+ write_collection_marker(list.size, TINY_LIST_MARKER_BASE, LIST_8_MARKER, LIST_16_MARKER, 'List')
123
102
 
124
103
  list.each { |item| pack(item) }
125
104
  end
126
105
 
106
+ # Writes the size marker shared by maps and lists (tiny / 8-bit / 16-bit).
107
+ def write_collection_marker(size, tiny_base, marker8, marker16, label)
108
+ if size < 16 # Tiny
109
+ write_marker([tiny_base | size].pack('C'))
110
+ elsif size < 256 # 8-bit size
111
+ write_marker([marker8, size].pack('CC'))
112
+ elsif size < 65_536 # 16-bit size ('n' is network byte order)
113
+ write_marker([marker16, size].pack('Cn'))
114
+ else
115
+ raise ProtocolError, "#{label} too large to pack (size: #{size})"
116
+ end
117
+ end
118
+
127
119
  def pack_integer(int)
128
120
  if int.between?(TINY_INT_MIN, TINY_INT_MAX) # Tiny Integer
129
121
  write_marker([int].pack('c')) # Signed char for range -128 to 127
@@ -54,7 +54,23 @@ module ActiveCypher
54
54
  # @param node_alias [Symbol] The alias used for the node in the query
55
55
  # @return [Hash] The hydrated attributes
56
56
  def hydrate_record(record, node_alias)
57
- raise NotImplementedError, "#{self.class} must implement #hydrate_record"
57
+ attrs = {}
58
+ node_data = record[node_alias] || record[node_alias.to_s]
59
+
60
+ if node_data.is_a?(Array) && node_data.length >= 2
61
+ properties_container = node_data[1]
62
+ if properties_container.is_a?(Array) && properties_container.length >= 3
63
+ properties = properties_container[2]
64
+ properties.each { |k, v| attrs[k.to_sym] = v } if properties.is_a?(Hash)
65
+ end
66
+ elsif node_data.is_a?(Hash)
67
+ node_data.each { |k, v| attrs[k.to_sym] = v }
68
+ elsif node_data.respond_to?(:properties)
69
+ attrs = node_data.properties.symbolize_keys
70
+ end
71
+
72
+ attrs[:internal_id] = record[:internal_id] || record['internal_id']
73
+ attrs
58
74
  end
59
75
 
60
76
  # Turns rows into symbols, because Rubyists fear strings.