activecypher 0.15.1 → 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: cb2fe676154a99ad8fc54cfe88731a1c7ce4759235e5103e0d87662c4cd450c2
4
- data.tar.gz: 8982b47d70418c6fcc2ec6e4e8a4bd9513df9ac94ab9c8d14b19f0b9af699c22
3
+ metadata.gz: dc6fb1ecc443117671c36c2b287b44d0059d706c8543ef113915fe0c67e8063d
4
+ data.tar.gz: 7f7a6e862f2c146dc19e89827e2b0ab2333bcc58f801b07a78e3e0c89f17fc6f
5
5
  SHA512:
6
- metadata.gz: 1d227c0f135e4203eee987c763b3d6bb2e61e4b24fbc3a5737166bbe1288d042d42d5cc2c83172f2df6d68f42480f76e8260a4571eee533cb53ce68c93f73228
7
- data.tar.gz: 2d47c7787d443326b89d8f734107f4492a2cc34f7930ae0c46fbd9e7fda3d1249ea0840376d3653218872ece6434151a557db7b430167a690824981abe54f523
6
+ metadata.gz: 6cd38267fc15f40e53c9d9d79d11fe52138cc20aa93290043e2aae7834616b6d78afb952a7c1c1ad6baf8e08531170618e43f297d70074c9db806928cd89b807
7
+ data.tar.gz: c2b0186231f35c838a56272ff6222f64af8a7be53891c8ed90260e297acdccef42992f28b65729ff61410172570cdcd92209c3c817d920d30341cc88906be9ff
@@ -45,7 +45,7 @@ module ActiveCypher
45
45
  # Returns the size, because counting things is the only certainty in life.
46
46
  #
47
47
  # @return [Integer] The number of records in the collection
48
- def size = load_target.size
48
+ def size = load_target.size
49
49
  alias length size
50
50
 
51
51
  # Fully refresh from the database.
@@ -285,6 +285,12 @@ module ActiveCypher
285
285
  instance_variable_set(instance_var, associate)
286
286
  end
287
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)
288
294
  # Define build method (e.g., build_author(name: "New Author"))
289
295
  define_method("build_#{name}") do |attributes = {}|
290
296
  target_class = target_class_name.constantize
@@ -431,25 +437,7 @@ module ActiveCypher
431
437
  instance_variable_set(instance_var, associate)
432
438
  end
433
439
 
434
- # Define build method (e.g., build_profile(data: {...}))
435
- define_method("build_#{name}") do |attributes = {}|
436
- target_class = target_class_name.constantize
437
- # TODO: Potentially set the inverse association reference here
438
- # For now, just instantiate the target class
439
- target_class.new(attributes)
440
- end
441
-
442
- # Define create method (e.g., create_profile(data: {...}))
443
- define_method("create_#{name}") do |attributes = {}|
444
- # Build the instance
445
- instance = public_send("build_#{name}", attributes)
446
- # Save the instance
447
- instance.save
448
- # If save is successful, associate it using the = method
449
- public_send("#{name}=", instance) if instance.persisted?
450
- # Return the instance
451
- instance
452
- end
440
+ define_build_and_create_methods(name, target_class_name)
453
441
  end
454
442
  end
455
443
 
@@ -82,7 +82,7 @@ module ActiveCypher
82
82
  # Wrap it all up in a fake-sane object string, so you can pretend your data is organized.
83
83
  "#<#{self.class} #{parts.join(', ')}>"
84
84
  end
85
-
85
+
86
86
  def internal_id
87
87
  working_id = super
88
88
  return working_id if working_id.nil?
@@ -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.
@@ -98,9 +98,9 @@ module ActiveCypher
98
98
  #
99
99
  # @yieldparam session [Bolt::Session] The session to use
100
100
  # @return [Object] The result of the block
101
- def with_session(**kw, &block)
101
+ def with_session(**kw, &)
102
102
  connect
103
- @driver.with_session(**kw, &block)
103
+ @driver.with_session(**kw, &)
104
104
  end
105
105
 
106
106
  # Asynchronously yields a Session from the connection pool.
@@ -108,9 +108,9 @@ module ActiveCypher
108
108
  #
109
109
  # @yieldparam session [Bolt::Session] The session to use
110
110
  # @return [Async::Task] A task that resolves to the block's result
111
- def async_with_session(**kw, &block)
111
+ def async_with_session(**kw, &)
112
112
  connect
113
- @driver.async_with_session(**kw, &block)
113
+ @driver.async_with_session(**kw, &)
114
114
  end
115
115
 
116
116
  # Runs a Cypher query via Bolt session.
@@ -169,26 +169,24 @@ module ActiveCypher
169
169
 
170
170
  begin
171
171
  result = Sync do
172
+ # Try to execute a simple query first
173
+ session = Bolt::Session.new(@connection)
174
+ session.run('RETURN 1 AS check', {})
175
+ session.close
176
+ true
177
+ rescue StandardError => e
178
+ # Query failed, need to reset the connection
179
+ logger.debug { "Connection needs reset: #{e.message}" }
180
+
181
+ # Send RESET message directly
172
182
  begin
173
- # Try to execute a simple query first
174
- session = Bolt::Session.new(@connection)
175
- session.run('RETURN 1 AS check', {})
176
- session.close
177
- true
178
- rescue StandardError => e
179
- # Query failed, need to reset the connection
180
- logger.debug { "Connection needs reset: #{e.message}" }
181
-
182
- # Send RESET message directly
183
- begin
184
- @connection.write_message(Bolt::Messaging::Reset.new)
185
- response = @connection.read_message
186
- logger.debug { "Reset response: #{response.class}" }
187
- response.is_a?(Bolt::Messaging::Success)
188
- rescue StandardError => reset_error
189
- logger.error { "Reset failed: #{reset_error.message}" }
190
- false
191
- end
183
+ @connection.write_message(Bolt::Messaging::Reset.new)
184
+ response = @connection.read_message
185
+ logger.debug { "Reset response: #{response.class}" }
186
+ response.is_a?(Bolt::Messaging::Success)
187
+ rescue StandardError => reset_error
188
+ logger.error { "Reset failed: #{reset_error.message}" }
189
+ false
192
190
  end
193
191
  end
194
192
  rescue StandardError => e
@@ -64,7 +64,7 @@ module ActiveCypher
64
64
  end
65
65
 
66
66
  def id_type_conversion(incoming)
67
- return incoming.to_i
67
+ incoming.to_i
68
68
  end
69
69
 
70
70
  # Memgraph uses different constraint syntax than Neo4j
@@ -204,30 +204,6 @@ module ActiveCypher
204
204
  metadata.compact
205
205
  end
206
206
 
207
- # Hydrates attributes from a Memgraph record
208
- # @param record [Hash] The raw record from Memgraph
209
- # @param node_alias [Symbol] The alias used for the node in the query
210
- # @return [Hash] The hydrated attributes
211
- def hydrate_record(record, node_alias)
212
- attrs = {}
213
- node_data = record[node_alias] || record[node_alias.to_s]
214
-
215
- if node_data.is_a?(Array) && node_data.length >= 2
216
- properties_container = node_data[1]
217
- if properties_container.is_a?(Array) && properties_container.length >= 3
218
- properties = properties_container[2]
219
- properties.each { |k, v| attrs[k.to_sym] = v } if properties.is_a?(Hash)
220
- end
221
- elsif node_data.is_a?(Hash)
222
- node_data.each { |k, v| attrs[k.to_sym] = v }
223
- elsif node_data.respond_to?(:properties)
224
- attrs = node_data.properties.symbolize_keys
225
- end
226
-
227
- attrs[:internal_id] = record[:internal_id] || record['internal_id']
228
- attrs
229
- end
230
-
231
207
  protected
232
208
 
233
209
  def parse_schema(rows)
@@ -130,30 +130,6 @@ module ActiveCypher
130
130
  metadata.compact
131
131
  end
132
132
 
133
- # Hydrates attributes from a Neo4j record
134
- # @param record [Hash] The raw record from Neo4j
135
- # @param node_alias [Symbol] The alias used for the node in the query
136
- # @return [Hash] The hydrated attributes
137
- def hydrate_record(record, node_alias)
138
- attrs = {}
139
- node_data = record[node_alias] || record[node_alias.to_s]
140
-
141
- if node_data.is_a?(Array) && node_data.length >= 2
142
- properties_container = node_data[1]
143
- if properties_container.is_a?(Array) && properties_container.length >= 3
144
- properties = properties_container[2]
145
- properties.each { |k, v| attrs[k.to_sym] = v } if properties.is_a?(Hash)
146
- end
147
- elsif node_data.is_a?(Hash)
148
- node_data.each { |k, v| attrs[k.to_sym] = v }
149
- elsif node_data.respond_to?(:properties)
150
- attrs = node_data.properties.symbolize_keys
151
- end
152
-
153
- attrs[:internal_id] = record[:internal_id] || record['internal_id']
154
- attrs
155
- end
156
-
157
133
  module Persistence
158
134
  include PersistenceMethods
159
135
 
@@ -92,14 +92,13 @@ module ActiveCypher
92
92
  end
93
93
 
94
94
  def create_uniqueness_constraint(label, *props, if_not_exists: true, name: nil)
95
+ props_clause = props.map { |p| "n.#{p}" }.join(', ')
95
96
  cypher = if connection.vendor == :memgraph
96
97
  # Memgraph syntax: CREATE CONSTRAINT ON (n:Label) ASSERT n.prop IS UNIQUE
97
98
  # Note: Memgraph doesn't support IF NOT EXISTS or named constraints
98
- props_clause = props.map { |p| "n.#{p}" }.join(', ')
99
99
  "CREATE CONSTRAINT ON (n:#{label}) ASSERT #{props_clause} IS UNIQUE"
100
100
  else
101
101
  # Neo4j syntax
102
- props_clause = props.map { |p| "n.#{p}" }.join(', ')
103
102
  c = +'CREATE CONSTRAINT'
104
103
  c << " #{name}" if name
105
104
  c << ' IF NOT EXISTS' if if_not_exists
@@ -113,7 +112,7 @@ module ActiveCypher
113
112
  cypher = if connection.vendor == :memgraph
114
113
  # Memgraph TEXT INDEX syntax (requires --experimental-enabled='text-search')
115
114
  # Memgraph only supports single property per text index, so create one per prop
116
- props.map.with_index do |p, i|
115
+ props.map.with_index do |p, _i|
117
116
  index_name = props.size > 1 ? "#{name}_#{p}" : name.to_s
118
117
  "CREATE TEXT INDEX #{index_name} ON :#{label}(#{p})"
119
118
  end
@@ -36,7 +36,7 @@ module ActiveCypher
36
36
  clear_changes_information
37
37
  end
38
38
  end
39
-
39
+
40
40
  def ==(other)
41
41
  # Compares by class and internal graph id only.
42
42
  # Note: an unsaved modification will still compare equal to the persisted version.
@@ -44,8 +44,6 @@ module ActiveCypher
44
44
 
45
45
  internal_id == other.internal_id
46
46
  end
47
-
48
-
49
47
  end
50
48
  end
51
49
  end
@@ -176,7 +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
+ # why is it private? This seems to be the only place it's called
180
180
  node_payload = model_class.connection
181
181
  .send(:process_node, node_payload)
182
182
  end
@@ -234,10 +234,10 @@ module ActiveCypher
234
234
  rid = row[:rid] || row['rid']
235
235
  from_node_id = (row[:from_node] || row['from_node'])&.dig(1, 0)
236
236
  to_node_id = (row[:to_node] || row['to_node'])&.dig(1, 0)
237
-
237
+
238
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)
239
+ from_node = Object.const_get(from_class).find(from_node_id)
240
+ to_node = Object.const_get(to_class).find(to_node_id)
241
241
 
242
242
  # Extract properties from the relationship data
243
243
  # Memgraph returns relationships wrapped as [type_code, [actual_data]]
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveCypher
4
- VERSION = '0.15.1'
4
+ VERSION = '0.15.2'
5
5
 
6
6
  def self.gem_version
7
7
  Gem::Version.new VERSION
@@ -9,11 +9,7 @@ module Cyrel
9
9
  # @param pattern [Cyrel::Pattern::Path, Cyrel::Pattern::Node, Cyrel::Pattern::Relationship]
10
10
  # The pattern to create. Typically a Path or Node.
11
11
  def initialize(pattern)
12
- # Ensure pattern is a valid type for CREATE
13
- unless pattern.is_a?(Cyrel::Pattern::Path) || pattern.is_a?(Cyrel::Pattern::Node) || pattern.is_a?(Cyrel::Pattern::Relationship)
14
- raise ArgumentError,
15
- "CREATE pattern must be a Cyrel::Pattern::Path, Node, or Relationship, got #{pattern.class}"
16
- end
12
+ Cyrel::Pattern.assert_pattern!(pattern, 'CREATE')
17
13
 
18
14
  # NOTE: Creating relationships between existing nodes requires coordination.
19
15
  # The pattern itself should reference existing aliases defined in a preceding MATCH/MERGE.
@@ -12,13 +12,7 @@ module Cyrel
12
12
  # @param path_variable [Symbol, String, nil] An optional variable to assign to the matched path.
13
13
  def initialize(pattern, optional: false, path_variable: nil)
14
14
  super() # Call super for Base initialization
15
- # Ensure pattern is a valid type
16
- unless pattern.is_a?(Cyrel::Pattern::Path) ||
17
- pattern.is_a?(Cyrel::Pattern::Node) ||
18
- pattern.is_a?(Cyrel::Pattern::Relationship)
19
- raise ArgumentError,
20
- "Match pattern must be a Cyrel::Pattern::Path, Node, or Relationship, got #{pattern.class}"
21
- end
15
+ Cyrel::Pattern.assert_pattern!(pattern, 'Match')
22
16
 
23
17
  @pattern = pattern
24
18
  @optional = optional
@@ -12,11 +12,7 @@ module Cyrel
12
12
  # @param pattern [Cyrel::Pattern::Path, Cyrel::Pattern::Node, Cyrel::Pattern::Relationship]
13
13
  # The pattern to merge. Typically a Path or Node.
14
14
  def initialize(pattern)
15
- # Ensure pattern is a valid type for MERGE
16
- unless pattern.is_a?(Cyrel::Pattern::Path) || pattern.is_a?(Cyrel::Pattern::Node) || pattern.is_a?(Cyrel::Pattern::Relationship)
17
- raise ArgumentError,
18
- "MERGE pattern must be a Cyrel::Pattern::Path, Node, or Relationship, got #{pattern.class}"
19
- end
15
+ Cyrel::Pattern.assert_pattern!(pattern, 'MERGE')
20
16
 
21
17
  @pattern = pattern
22
18
  end
@@ -13,10 +13,7 @@ module Cyrel
13
13
  # @param pattern [Cyrel::Pattern::Path, Cyrel::Pattern::Node, Cyrel::Pattern::Relationship]
14
14
  # The pattern to check for existence.
15
15
  def initialize(pattern)
16
- unless pattern.is_a?(Cyrel::Pattern::Path) || pattern.is_a?(Cyrel::Pattern::Node) || pattern.is_a?(Cyrel::Pattern::Relationship)
17
- raise ArgumentError,
18
- "EXISTS pattern must be a Cyrel::Pattern::Path, Node, or Relationship, got #{pattern.class}"
19
- end
16
+ Cyrel::Pattern.assert_pattern!(pattern, 'EXISTS')
20
17
 
21
18
  @pattern = pattern
22
19
  end
@@ -13,10 +13,7 @@ module Cyrel
13
13
  # @param projection_expression [Cyrel::Expression::Base, Object]
14
14
  # The expression evaluated for each match of the pattern.
15
15
  def initialize(pattern, projection_expression)
16
- unless pattern.is_a?(Cyrel::Pattern::Path) || pattern.is_a?(Cyrel::Pattern::Node) || pattern.is_a?(Cyrel::Pattern::Relationship)
17
- raise ArgumentError,
18
- "Pattern Comprehension pattern must be a Path, Node, or Relationship, got #{pattern.class}"
19
- end
16
+ Cyrel::Pattern.assert_pattern!(pattern, 'Pattern Comprehension')
20
17
 
21
18
  @pattern = pattern
22
19
  @projection_expression = Expression.coerce(projection_expression)
@@ -12,7 +12,7 @@ module Cyrel
12
12
 
13
13
  attribute :alias_name, Cyrel::Types::SymbolType.new
14
14
  attribute :labels, array: :string, default: []
15
- attribute :or_labels, array: :string, default: [] # Memgraph 3.2+: (n:Label1|Label2)
15
+ attribute :or_labels, array: :string, default: [] # Memgraph 3.2+: (n:Label1|Label2)
16
16
  attribute :properties, Cyrel::Types::HashType.new, default: -> { {} }
17
17
 
18
18
  validates :alias_name, presence: true
data/lib/cyrel/pattern.rb CHANGED
@@ -4,5 +4,15 @@ module Cyrel
4
4
  # Namespace for classes representing structural components of Cypher patterns
5
5
  # (nodes, relationships, paths).
6
6
  module Pattern
7
+ # Validates that the given object is a usable pattern (Path, Node, or Relationship).
8
+ # @param pattern [Object] The object to validate.
9
+ # @param context [String] Label used in the error message (e.g. "CREATE", "MATCH").
10
+ # @raise [ArgumentError] When the object is not a pattern.
11
+ def self.assert_pattern!(pattern, context)
12
+ return pattern if pattern.is_a?(Path) || pattern.is_a?(Node) || pattern.is_a?(Relationship)
13
+
14
+ raise ArgumentError,
15
+ "#{context} pattern must be a Cyrel::Pattern::Path, Node, or Relationship, got #{pattern.class}"
16
+ end
7
17
  end
8
18
  end
data/lib/cyrel/query.rb CHANGED
@@ -136,13 +136,7 @@ module Cyrel
136
136
  # ------------------------------------------------------------------
137
137
  processed_conditions = conditions.flat_map do |cond|
138
138
  if cond.is_a?(Hash)
139
- cond.map do |key, value|
140
- Expression::Comparison.new(
141
- Expression::PropertyAccess.new(@current_alias || infer_alias, key),
142
- :'=',
143
- value
144
- )
145
- end
139
+ hash_to_conditions(cond)
146
140
  else
147
141
  cond # already an expression (or coercible)
148
142
  end
@@ -155,7 +149,7 @@ module Cyrel
155
149
  # ------------------------------------------------------------------
156
150
  # 2. Merge with an existing WHERE (if any)
157
151
  # ------------------------------------------------------------------
158
- existing_where_index = @clauses.find_index { |c| c.is_a?(Clause::Where) || (c.is_a?(AST::ClauseAdapter) && c.ast_node.is_a?(AST::WhereNode)) }
152
+ existing_where_index = find_clause_index(Clause::Where, AST::WhereNode)
159
153
 
160
154
  if existing_where_index
161
155
  existing_clause = @clauses[existing_where_index]
@@ -258,7 +252,7 @@ module Cyrel
258
252
  ast_clause = AST::ClauseAdapter.new(set_node)
259
253
 
260
254
  # Check for existing SET clause to merge with
261
- existing_set_index = @clauses.find_index { |c| c.is_a?(Clause::Set) || (c.is_a?(AST::ClauseAdapter) && c.ast_node.is_a?(AST::SetNode)) }
255
+ existing_set_index = find_clause_index(Clause::Set, AST::SetNode)
262
256
 
263
257
  if existing_set_index
264
258
  existing_clause = @clauses[existing_set_index]
@@ -321,58 +315,19 @@ module Cyrel
321
315
  # @return [self]
322
316
  # Because sometimes you want to pass things along, and sometimes you just want to pass the buck.
323
317
  def with(*items, distinct: false, where: nil)
324
- # Process items similar to existing Return clause
325
- processed_items = items.flatten.map do |item|
326
- case item
327
- when Expression::Base
328
- item
329
- when Symbol
330
- # Create a RawIdentifier for variable names
331
- Clause::Return::RawIdentifier.new(item.to_s)
332
- when String
333
- # Check if string looks like property access (e.g. "person.name")
334
- # If so, treat as raw identifier, otherwise parameterize
335
- if item.match?(/\A\w+\.\w+\z/)
336
- Clause::Return::RawIdentifier.new(item)
337
- else
338
- # String literals should be coerced to expressions (parameterized)
339
- Expression.coerce(item)
340
- end
341
- else
342
- Expression.coerce(item)
343
- end
344
- end
318
+ processed_items = process_projection_items(items)
345
319
 
346
320
  # Process WHERE conditions if provided
347
321
  where_conditions = case where
348
322
  when nil then []
349
- when Hash
350
- # Convert hash to equality comparisons
351
- where.map do |key, value|
352
- Expression::Comparison.new(
353
- Expression::PropertyAccess.new(@current_alias || infer_alias, key),
354
- :'=',
355
- value
356
- )
357
- end
323
+ when Hash then hash_to_conditions(where)
358
324
  when Array then where
359
325
  else [where] # Single condition
360
326
  end
361
327
 
362
328
  # Use AST-based implementation
363
329
  with_node = AST::WithNode.new(processed_items, distinct: distinct, where_conditions: where_conditions)
364
- ast_clause = AST::ClauseAdapter.new(with_node)
365
-
366
- # Find and replace existing with or add new one
367
- existing_with_index = @clauses.find_index { |c| c.is_a?(Clause::With) || (c.is_a?(AST::ClauseAdapter) && c.ast_node.is_a?(AST::WithNode)) }
368
-
369
- if existing_with_index
370
- @clauses[existing_with_index] = ast_clause
371
- else
372
- add_clause(ast_clause)
373
- end
374
-
375
- self
330
+ replace_or_add_clause(AST::ClauseAdapter.new(with_node), Clause::With, AST::WithNode)
376
331
  end
377
332
 
378
333
  # Adds a RETURN clause.
@@ -384,41 +339,11 @@ module Cyrel
384
339
  # is a reserved keyword in Ruby. We're not crazy - we just want to provide
385
340
  # a clean DSL while respecting Ruby's language constraints.
386
341
  def return_(*items, distinct: false)
387
- # Process items similar to existing Return clause
388
- processed_items = items.flatten.map do |item|
389
- case item
390
- when Expression::Base
391
- item
392
- when Symbol
393
- # Create a RawIdentifier for variable names
394
- Clause::Return::RawIdentifier.new(item.to_s)
395
- when String
396
- # Check if string looks like property access (e.g. "person.name")
397
- # If so, treat as raw identifier, otherwise parameterize
398
- if item.match?(/\A\w+\.\w+\z/)
399
- Clause::Return::RawIdentifier.new(item)
400
- else
401
- # String literals should be coerced to expressions (parameterized)
402
- Expression.coerce(item)
403
- end
404
- else
405
- Expression.coerce(item)
406
- end
407
- end
342
+ processed_items = process_projection_items(items)
408
343
 
409
344
  # Use AST-based implementation
410
345
  return_node = AST::ReturnNode.new(processed_items, distinct: distinct)
411
- ast_clause = AST::ClauseAdapter.new(return_node)
412
-
413
- # Find and replace existing return or add new one
414
- existing_return_index = @clauses.find_index { |c| c.is_a?(Clause::Return) || (c.is_a?(AST::ClauseAdapter) && c.ast_node.is_a?(AST::ReturnNode)) }
415
-
416
- if existing_return_index
417
- @clauses[existing_return_index] = ast_clause
418
- else
419
- add_clause(ast_clause)
420
- end
421
- self
346
+ replace_or_add_clause(AST::ClauseAdapter.new(return_node), Clause::Return, AST::ReturnNode)
422
347
  end
423
348
 
424
349
  # Adds or replaces the ORDER BY clause.
@@ -432,17 +357,7 @@ module Cyrel
432
357
 
433
358
  # Use AST-based implementation
434
359
  order_by_node = AST::OrderByNode.new(items_array)
435
- ast_clause = AST::ClauseAdapter.new(order_by_node)
436
-
437
- # Find and replace existing order by or add new one
438
- existing_order_index = @clauses.find_index { |c| c.is_a?(Clause::OrderBy) || (c.is_a?(AST::ClauseAdapter) && c.ast_node.is_a?(AST::OrderByNode)) }
439
-
440
- if existing_order_index
441
- @clauses[existing_order_index] = ast_clause
442
- else
443
- add_clause(ast_clause)
444
- end
445
- self
360
+ replace_or_add_clause(AST::ClauseAdapter.new(order_by_node), Clause::OrderBy, AST::OrderByNode)
446
361
  end
447
362
 
448
363
  # Adds or replaces the SKIP clause.
@@ -452,17 +367,7 @@ module Cyrel
452
367
  def skip(amount)
453
368
  # Use AST-based implementation
454
369
  skip_node = AST::SkipNode.new(amount)
455
- ast_clause = AST::ClauseAdapter.new(skip_node)
456
-
457
- # Find and replace existing skip or add new one
458
- existing_skip_index = @clauses.find_index { |c| c.is_a?(Clause::Skip) || (c.is_a?(AST::ClauseAdapter) && c.ast_node.is_a?(AST::SkipNode)) }
459
-
460
- if existing_skip_index
461
- @clauses[existing_skip_index] = ast_clause
462
- else
463
- add_clause(ast_clause)
464
- end
465
- self
370
+ replace_or_add_clause(AST::ClauseAdapter.new(skip_node), Clause::Skip, AST::SkipNode)
466
371
  end
467
372
 
468
373
  # Adds or replaces the LIMIT clause.
@@ -472,18 +377,7 @@ module Cyrel
472
377
  def limit(amount)
473
378
  # Use AST-based implementation
474
379
  limit_node = AST::LimitNode.new(amount)
475
- ast_clause = AST::ClauseAdapter.new(limit_node)
476
-
477
- # Find and replace existing limit or add new one
478
- existing_limit_index = @clauses.find_index { |c| c.is_a?(Clause::Limit) || (c.is_a?(AST::ClauseAdapter) && c.ast_node.is_a?(AST::LimitNode)) }
479
-
480
- if existing_limit_index
481
- @clauses[existing_limit_index] = ast_clause
482
- else
483
- add_clause(ast_clause)
484
- end
485
-
486
- self
380
+ replace_or_add_clause(AST::ClauseAdapter.new(limit_node), Clause::Limit, AST::LimitNode)
487
381
  end
488
382
 
489
383
  # Adds a CALL procedure clause.
@@ -600,6 +494,58 @@ module Cyrel
600
494
  add_clause(AST::ClauseAdapter.new(load_csv_node))
601
495
  end
602
496
 
497
+ # Coerces projection items (symbols, strings, expressions) for WITH/RETURN clauses.
498
+ def process_projection_items(items)
499
+ items.flatten.map do |item|
500
+ case item
501
+ when Expression::Base
502
+ item
503
+ when Symbol
504
+ # Create a RawIdentifier for variable names
505
+ Clause::Return::RawIdentifier.new(item.to_s)
506
+ when String
507
+ # Check if string looks like property access (e.g. "person.name")
508
+ # If so, treat as raw identifier, otherwise parameterize
509
+ if item.match?(/\A\w+\.\w+\z/)
510
+ Clause::Return::RawIdentifier.new(item)
511
+ else
512
+ # String literals should be coerced to expressions (parameterized)
513
+ Expression.coerce(item)
514
+ end
515
+ else
516
+ Expression.coerce(item)
517
+ end
518
+ end
519
+ end
520
+
521
+ # Converts a Hash into equality comparisons against the current alias.
522
+ def hash_to_conditions(hash)
523
+ hash.map do |key, value|
524
+ Expression::Comparison.new(
525
+ Expression::PropertyAccess.new(@current_alias || infer_alias, key),
526
+ :'=',
527
+ value
528
+ )
529
+ end
530
+ end
531
+
532
+ # Finds the index of an existing clause matching either the legacy clause
533
+ # class or an AST::ClauseAdapter wrapping the given AST node class.
534
+ def find_clause_index(clause_class, ast_node_class)
535
+ @clauses.find_index { |c| c.is_a?(clause_class) || (c.is_a?(AST::ClauseAdapter) && c.ast_node.is_a?(ast_node_class)) }
536
+ end
537
+
538
+ # Replaces an existing matching clause in place, or appends the new one.
539
+ def replace_or_add_clause(ast_clause, clause_class, ast_node_class)
540
+ existing_index = find_clause_index(clause_class, ast_node_class)
541
+ if existing_index
542
+ @clauses[existing_index] = ast_clause
543
+ else
544
+ add_clause(ast_clause)
545
+ end
546
+ self
547
+ end
548
+
603
549
  # private
604
550
 
605
551
  # Merges parameters from another query, ensuring keys are unique.
data/lib/cyrel.rb CHANGED
@@ -93,55 +93,34 @@ module Cyrel
93
93
  # When called like: node(:a) > rel(:r) > node(:b)
94
94
  # The rel(:r) is evaluated first, then > is called
95
95
  # So we need to modify the last relationship that was just added
96
- if @elements.last.is_a?(Cyrel::Pattern::Relationship)
97
- # Replace the last relationship with one that has the correct direction
98
- last_rel = @elements.pop
99
- new_rel = Cyrel::Pattern::Relationship.new(
100
- alias_name: last_rel.alias_name,
101
- types: last_rel.types,
102
- properties: last_rel.properties,
103
- length: last_rel.length,
104
- direction: :outgoing
105
- )
106
- @elements << new_rel
107
- else
108
- @pending_direction = :outgoing
109
- end
110
- self
96
+ apply_direction(:outgoing)
111
97
  end
112
98
 
113
99
  def <(_other)
114
100
  # Same logic as > but for incoming direction
115
- if @elements.last.is_a?(Cyrel::Pattern::Relationship)
116
- last_rel = @elements.pop
117
- new_rel = Cyrel::Pattern::Relationship.new(
118
- alias_name: last_rel.alias_name,
119
- types: last_rel.types,
120
- properties: last_rel.properties,
121
- length: last_rel.length,
122
- direction: :incoming
123
- )
124
- @elements << new_rel
125
- else
126
- @pending_direction = :incoming
127
- end
128
- self
101
+ apply_direction(:incoming)
129
102
  end
130
103
 
131
104
  def -(_other)
132
105
  # Same logic as > but for bidirectional
106
+ apply_direction(:both)
107
+ end
108
+
109
+ private
110
+
111
+ def apply_direction(direction)
133
112
  if @elements.last.is_a?(Cyrel::Pattern::Relationship)
113
+ # Replace the last relationship with one that has the correct direction
134
114
  last_rel = @elements.pop
135
- new_rel = Cyrel::Pattern::Relationship.new(
115
+ @elements << Cyrel::Pattern::Relationship.new(
136
116
  alias_name: last_rel.alias_name,
137
117
  types: last_rel.types,
138
118
  properties: last_rel.properties,
139
119
  length: last_rel.length,
140
- direction: :both
120
+ direction: direction
141
121
  )
142
- @elements << new_rel
143
122
  else
144
- @pending_direction = :both
123
+ @pending_direction = direction
145
124
  end
146
125
  self
147
126
  end
@@ -256,9 +235,9 @@ module Cyrel
256
235
  # Example:
257
236
  # Cyrel.exists_block { match(Cyrel.node(:a) > Cyrel.rel(:r) > Cyrel.node(:b, :Admin)) }
258
237
  # # => EXISTS { MATCH (a)-[r]->(b:Admin) }
259
- def exists_block(&block)
238
+ def exists_block(&)
260
239
  subquery = Query.new
261
- subquery.instance_eval(&block)
240
+ subquery.instance_eval(&)
262
241
  Expression::ExistsBlock.new(subquery)
263
242
  end
264
243
 
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.15.1
4
+ version: 0.15.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih