activecypher 0.10.4 → 0.11.0

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: 45703ee8a46ca012c9c4e6bd82708f3846bdea0fae15c4fb7fbff7a46d0fb3b3
4
- data.tar.gz: 93e134bd2615288003c16c94159344b48023d3d593f45e47441483a5be3c02b1
3
+ metadata.gz: 2396c4b661d5d743f6c35c7921d020f096531f543c7df9b99e05bddf84c9a2db
4
+ data.tar.gz: 51e407abc0ac00aaa315fd8acdf489fad8a8d8ae3a46702df4b97c8347ea3fbd
5
5
  SHA512:
6
- metadata.gz: 9232c48290ea388ad7c49b4a3dfb4ffffc5f4493c33fa3000ad7936c40730f5c6b106a9190b61a52576d80f09c4632a903696f0c0c683332fd6c6325a2c9d7af
7
- data.tar.gz: d48c94cf08b6aa3d1a7a9659b13fdde62fdb0ca444a281e97ccea3e28646bc0520702e3f90df59a0e9b2650b48a65e6170cedea896e2a7402901d6df7a5615dc
6
+ metadata.gz: 210b7c45581497c2ab69a3609e452dba832c0b15ccd3dbf1ee81740f7e4a4c693c537745ea9de13b3904f4c2c5c8386fecfd3fa45eab7e98ce2e7c8976b85cf6
7
+ data.tar.gz: 29df9c560d01ec8ce6e8c7eee1e977d6a1b61dc532c7af929b3da101fbf9f3440afdd4332a9b7cc2c2f4af1ab420e51bcc21f55841e9a1b57f94120371459610
@@ -230,23 +230,13 @@ module ActiveCypher
230
230
  def viable?
231
231
  return false unless connected?
232
232
 
233
- # Perform a lightweight check to verify the connection is still functional
234
- begin
235
- # Try to send a simple NOOP query to check connection health
236
- write_message(Messaging::Run.new('RETURN 1', {}, {}), 'VIABILITY_CHECK')
237
- read_message
238
-
239
- # Reset the connection state
240
- reset!
233
+ # Use the health check method to determine viability
234
+ health = health_check
241
235
 
242
- # If we got a successful response, the connection is viable
236
+ if health[:healthy]
243
237
  true
244
- rescue ConnectionError, ProtocolError
245
- # If the connection is broken, close it and return false
246
- close
247
- false
248
- rescue StandardError
249
- # For any other errors, also consider the connection non-viable
238
+ else
239
+ # If health check failed, close the connection
250
240
  close
251
241
  false
252
242
  end
@@ -480,6 +470,110 @@ module ActiveCypher
480
470
  Bolt::Session.new(self, **)
481
471
  end
482
472
 
473
+ # Synchronously execute a read transaction.
474
+ def read_transaction(db: nil, timeout: nil, metadata: nil, &)
475
+ session(database: db).read_transaction(db: db, timeout: timeout, metadata: metadata, &)
476
+ end
477
+
478
+ # Synchronously execute a write transaction.
479
+ def write_transaction(db: nil, timeout: nil, metadata: nil, &)
480
+ session(database: db).write_transaction(db: db, timeout: timeout, metadata: metadata, &)
481
+ end
482
+
483
+ # Asynchronously execute a read transaction.
484
+ def async_read_transaction(db: nil, timeout: nil, metadata: nil, &)
485
+ session(database: db).async_read_transaction(db: db, timeout: timeout, metadata: metadata, &)
486
+ end
487
+
488
+ # Asynchronously execute a write transaction.
489
+ def async_write_transaction(db: nil, timeout: nil, metadata: nil, &)
490
+ session(database: db).async_write_transaction(db: db, timeout: timeout, metadata: metadata, &)
491
+ end
492
+
493
+ # ────────────────────────────────────────────────────────────────────
494
+ # HEALTH AND VERSION DETECTION METHODS
495
+ # ────────────────────────────────────────────────────────────────────
496
+
497
+ # Returns parsed version information from the server agent string.
498
+ #
499
+ # @return [Hash] version information with :database_type, :version, :major, :minor, :patch
500
+ # @note Extracts version from server_agent captured during handshake
501
+ def version
502
+ return @version if defined?(@version)
503
+
504
+ @version = parse_version_from_server_agent
505
+ end
506
+
507
+ # Returns the database type detected from server agent.
508
+ #
509
+ # @return [Symbol] :neo4j, :memgraph, or :unknown
510
+ def database_type
511
+ version[:database_type]
512
+ end
513
+
514
+ # Performs a health check using database-appropriate queries.
515
+ #
516
+ # @return [Hash] health check result with :healthy, :response_time_ms, :details
517
+ # @note Uses different queries based on detected database type
518
+ def health_check
519
+ return { healthy: false, response_time_ms: nil, details: 'Not connected' } unless connected?
520
+
521
+ result = nil
522
+
523
+ begin
524
+ Async do
525
+ result = case database_type
526
+ when :neo4j
527
+ perform_neo4j_health_check
528
+ when :memgraph
529
+ perform_memgraph_health_check
530
+ else
531
+ perform_generic_health_check
532
+ end
533
+ end.wait
534
+
535
+ result
536
+ rescue ConnectionError, ProtocolError => e
537
+ { healthy: false, response_time_ms: nil, details: "Health check failed: #{e.message}" }
538
+ end
539
+ end
540
+
541
+ # Returns comprehensive database information.
542
+ #
543
+ # @return [Hash] database information including version, health, and system details
544
+ def database_info
545
+ info = version.dup
546
+ health = health_check
547
+
548
+ info.merge({
549
+ healthy: health[:healthy],
550
+ response_time_ms: health[:response_time_ms],
551
+ server_agent: @server_agent,
552
+ connection_id: @connection_id,
553
+ protocol_version: @protocol_version
554
+ })
555
+ end
556
+
557
+ # Returns server/cluster status information (when available).
558
+ #
559
+ # @return [Array<Hash>, nil] array of server status objects or nil if not supported
560
+ def server_status
561
+ return nil unless connected?
562
+
563
+ begin
564
+ case database_type
565
+ when :neo4j
566
+ get_neo4j_server_status
567
+ when :memgraph
568
+ get_memgraph_server_status
569
+ else
570
+ nil
571
+ end
572
+ rescue ConnectionError, ProtocolError
573
+ nil # Gracefully handle unsupported operations
574
+ end
575
+ end
576
+
483
577
  # ────────────────────────────────────────────────────────────────────
484
578
  # PRIVATE HELPER METHODS
485
579
  # ────────────────────────────────────────────────────────────────────
@@ -540,6 +634,317 @@ module ActiveCypher
540
634
  # @return [Boolean]
541
635
  # @note The Schrödinger's cat of sockets.
542
636
  def socket_open? = @socket && !@socket.closed?
637
+
638
+ # ────────────────────────────────────────────────────────────────────
639
+ # HEALTH AND VERSION DETECTION HELPERS
640
+ # ────────────────────────────────────────────────────────────────────
641
+
642
+ # Parses version information from the server_agent string.
643
+ #
644
+ # @return [Hash] parsed version information
645
+ def parse_version_from_server_agent
646
+ return default_version_info unless @server_agent
647
+
648
+ case @server_agent
649
+ when %r{^Neo4j/(\d+\.\d+(?:\.\d+)?)}i
650
+ version_string = ::Regexp.last_match(1)
651
+ parts = version_string.split('.').map(&:to_i)
652
+ {
653
+ database_type: :neo4j,
654
+ version: version_string,
655
+ major: parts[0] || 0,
656
+ minor: parts[1] || 0,
657
+ patch: parts[2] || 0
658
+ }
659
+ when %r{^Memgraph/(\d+\.\d+(?:\.\d+)?)}i
660
+ version_string = ::Regexp.last_match(1)
661
+ parts = version_string.split('.').map(&:to_i)
662
+ {
663
+ database_type: :memgraph,
664
+ version: version_string,
665
+ major: parts[0] || 0,
666
+ minor: parts[1] || 0,
667
+ patch: parts[2] || 0
668
+ }
669
+ when /.*Memgraph/i
670
+ # Handle Memgraph server agent: "Neo4j/v5.11.0 compatible graph database server - Memgraph"
671
+ if @server_agent =~ %r{Neo4j/v(\d+\.\d+(?:\.\d+)?)}
672
+ version_string = ::Regexp.last_match(1)
673
+ parts = version_string.split('.').map(&:to_i)
674
+ {
675
+ database_type: :memgraph,
676
+ version: version_string,
677
+ major: parts[0] || 0,
678
+ minor: parts[1] || 0,
679
+ patch: parts[2] || 0
680
+ }
681
+ else
682
+ {
683
+ database_type: :memgraph,
684
+ version: 'unknown',
685
+ major: 0,
686
+ minor: 0,
687
+ patch: 0
688
+ }
689
+ end
690
+ else
691
+ {
692
+ database_type: :unknown,
693
+ version: @server_agent,
694
+ major: 0,
695
+ minor: 0,
696
+ patch: 0
697
+ }
698
+ end
699
+ end
700
+
701
+ # Returns default version info when server_agent is not available.
702
+ #
703
+ # @return [Hash] default version information
704
+ def default_version_info
705
+ {
706
+ database_type: :unknown,
707
+ version: 'unknown',
708
+ major: 0,
709
+ minor: 0,
710
+ patch: 0
711
+ }
712
+ end
713
+
714
+ # Performs Neo4j-specific health check using RETURN 1 (fallback from db.ping).
715
+ #
716
+ # @return [Hash] health check result
717
+ def perform_neo4j_health_check
718
+ start_time = Time.now
719
+
720
+ begin
721
+ write_message(Messaging::Run.new('RETURN 1 AS result', {}, {}), 'HEALTH_CHECK')
722
+ run_response = read_message
723
+
724
+ case run_response
725
+ when Messaging::Success
726
+ # Send PULL message to complete the transaction
727
+ write_message(Messaging::Pull.new({ 'n' => -1 }), 'PULL')
728
+
729
+ # Read messages until we get SUCCESS or FAILURE
730
+ response_time = nil
731
+ loop do
732
+ msg = read_message
733
+ case msg
734
+ when Messaging::Record
735
+ # Skip records, we just want to know if the query succeeded
736
+ next
737
+ when Messaging::Success
738
+ response_time = ((Time.now - start_time) * 1000).round(2)
739
+ return { healthy: true, response_time_ms: response_time, details: 'RETURN 1 succeeded' }
740
+ when Messaging::Failure
741
+ return { healthy: false, response_time_ms: nil, details: 'RETURN 1 failed with error' }
742
+ else
743
+ return { healthy: false, response_time_ms: nil, details: 'RETURN 1 unexpected response' }
744
+ end
745
+ end
746
+ else
747
+ { healthy: false, response_time_ms: nil, details: 'RETURN 1 run failed' }
748
+ end
749
+ ensure
750
+ # Reset connection state after health check
751
+ reset!
752
+ end
753
+ end
754
+
755
+ # Performs Memgraph-specific health check using SHOW STORAGE INFO.
756
+ #
757
+ # @return [Hash] health check result
758
+ def perform_memgraph_health_check
759
+ start_time = Time.now
760
+
761
+ begin
762
+ write_message(Messaging::Run.new('SHOW STORAGE INFO', {}, {}), 'HEALTH_CHECK')
763
+ run_response = read_message
764
+
765
+ case run_response
766
+ when Messaging::Success
767
+ # Send PULL message to complete the transaction
768
+ write_message(Messaging::Pull.new({ 'n' => -1 }), 'PULL')
769
+
770
+ # Read messages until we get SUCCESS or FAILURE
771
+ response_time = nil
772
+ loop do
773
+ msg = read_message
774
+ case msg
775
+ when Messaging::Record
776
+ # Skip records, we just want to know if the query succeeded
777
+ next
778
+ when Messaging::Success
779
+ response_time = ((Time.now - start_time) * 1000).round(2)
780
+ return { healthy: true, response_time_ms: response_time, details: 'SHOW STORAGE INFO succeeded' }
781
+ when Messaging::Failure
782
+ return { healthy: false, response_time_ms: nil, details: 'SHOW STORAGE INFO failed with error' }
783
+ else
784
+ return { healthy: false, response_time_ms: nil, details: 'SHOW STORAGE INFO unexpected response' }
785
+ end
786
+ end
787
+ else
788
+ { healthy: false, response_time_ms: nil, details: 'SHOW STORAGE INFO run failed' }
789
+ end
790
+ ensure
791
+ # Reset connection state after health check
792
+ reset!
793
+ end
794
+ end
795
+
796
+ # Performs generic health check using simple RETURN 1 query.
797
+ #
798
+ # @return [Hash] health check result
799
+ def perform_generic_health_check
800
+ start_time = Time.now
801
+
802
+ begin
803
+ write_message(Messaging::Run.new('RETURN 1', {}, {}), 'HEALTH_CHECK')
804
+ run_response = read_message
805
+
806
+ case run_response
807
+ when Messaging::Success
808
+ # Send PULL message to complete the transaction
809
+ write_message(Messaging::Pull.new({ 'n' => -1 }), 'PULL')
810
+
811
+ # Read messages until we get SUCCESS or FAILURE
812
+ response_time = nil
813
+ loop do
814
+ msg = read_message
815
+ case msg
816
+ when Messaging::Record
817
+ # Skip records, we just want to know if the query succeeded
818
+ next
819
+ when Messaging::Success
820
+ response_time = ((Time.now - start_time) * 1000).round(2)
821
+ return { healthy: true, response_time_ms: response_time, details: 'RETURN 1 succeeded' }
822
+ when Messaging::Failure
823
+ return { healthy: false, response_time_ms: nil, details: 'RETURN 1 failed with error' }
824
+ else
825
+ return { healthy: false, response_time_ms: nil, details: 'RETURN 1 unexpected response' }
826
+ end
827
+ end
828
+ else
829
+ { healthy: false, response_time_ms: nil, details: 'RETURN 1 run failed' }
830
+ end
831
+ ensure
832
+ # Reset connection state after health check
833
+ reset!
834
+ end
835
+ end
836
+
837
+ # Gets Neo4j server status using SHOW SERVERS query.
838
+ #
839
+ # @return [Array<Hash>] server status information
840
+ def get_neo4j_server_status
841
+ write_message(Messaging::Run.new('SHOW SERVERS', {}, {}), 'SERVER_STATUS')
842
+ response = read_message
843
+
844
+ case response
845
+ when Messaging::Success
846
+ servers = []
847
+
848
+ # Read records until we get SUCCESS
849
+ loop do
850
+ record_response = read_message
851
+ case record_response
852
+ when Messaging::Record
853
+ # Parse server record - this is a simplified version
854
+ servers << parse_neo4j_server_record(record_response)
855
+ when Messaging::Success
856
+ break
857
+ else
858
+ break
859
+ end
860
+ end
861
+
862
+ servers
863
+ else
864
+ []
865
+ end
866
+ ensure
867
+ reset!
868
+ end
869
+
870
+ # Gets Memgraph server status using SHOW DATABASES query.
871
+ #
872
+ # @return [Array<Hash>] database status information
873
+ def get_memgraph_server_status
874
+ write_message(Messaging::Run.new('SHOW DATABASES', {}, {}), 'SERVER_STATUS')
875
+ response = read_message
876
+
877
+ case response
878
+ when Messaging::Success
879
+ databases = []
880
+
881
+ # Read records until we get SUCCESS
882
+ loop do
883
+ record_response = read_message
884
+ case record_response
885
+ when Messaging::Record
886
+ # Parse database record
887
+ databases << parse_memgraph_database_record(record_response)
888
+ when Messaging::Success
889
+ break
890
+ else
891
+ break
892
+ end
893
+ end
894
+
895
+ databases
896
+ else
897
+ # Fallback to SHOW DATABASE for single database info
898
+ get_memgraph_current_database
899
+ end
900
+ ensure
901
+ reset!
902
+ end
903
+
904
+ # Gets current Memgraph database info using SHOW DATABASE.
905
+ #
906
+ # @return [Array<Hash>] current database information
907
+ def get_memgraph_current_database
908
+ write_message(Messaging::Run.new('SHOW DATABASE', {}, {}), 'CURRENT_DATABASE')
909
+ response = read_message
910
+
911
+ case response
912
+ when Messaging::Success
913
+ [{ name: 'current', status: 'active', type: 'memgraph' }]
914
+ else
915
+ []
916
+ end
917
+ ensure
918
+ reset!
919
+ end
920
+
921
+ # Parses a Neo4j server record from SHOW SERVERS result.
922
+ #
923
+ # @param record [Messaging::Record] the record message
924
+ # @return [Hash] parsed server information
925
+ def parse_neo4j_server_record(_record)
926
+ # Simplified parsing - in reality this would extract specific fields
927
+ {
928
+ name: 'server',
929
+ address: "#{@host}:#{@port}",
930
+ state: 'Enabled',
931
+ health: 'Available',
932
+ type: 'neo4j'
933
+ }
934
+ end
935
+
936
+ # Parses a Memgraph database record from SHOW DATABASES result.
937
+ #
938
+ # @param record [Messaging::Record] the record message
939
+ # @return [Hash] parsed database information
940
+ def parse_memgraph_database_record(_record)
941
+ # Simplified parsing
942
+ {
943
+ name: 'database',
944
+ status: 'active',
945
+ type: 'memgraph'
946
+ }
947
+ end
543
948
  end
544
949
  end
545
950
  end
@@ -119,54 +119,35 @@ module ActiveCypher
119
119
  # @param metadata [Hash] Transaction metadata to send to the server.
120
120
  # @yield [tx] The transaction to use for queries.
121
121
  # @return The result of the block.
122
- def run_transaction(mode = :write, db: nil, timeout: nil, metadata: nil, &)
123
- # Ensure we're running in an Async context
122
+ def run_transaction(mode = :write, db: nil, timeout: nil, metadata: nil, &block)
124
123
  if Async::Task.current?
125
- # Already in an Async task, proceed normally
126
- begin
127
- execute_transaction(mode, db: db, timeout: timeout, metadata: metadata, &)
128
- rescue StandardError => e
129
- # Ensure errors are properly propagated
130
- raise e
131
- end
124
+ # Already in an async context, just run the block.
125
+ # The block will run asynchronously within the current task.
126
+ _execute_transaction_block(mode, db, timeout, metadata, &block)
132
127
  else
133
- # Wrap in an Async task
134
- result = nil
135
- error = nil
136
-
128
+ # Not in an async context, so we need to create one and wait for it to complete.
137
129
  Async do
138
- result = execute_transaction(mode, db: db, timeout: timeout, metadata: metadata, &)
139
- rescue StandardError => e
140
- error = e
130
+ _execute_transaction_block(mode, db, timeout, metadata, &block)
141
131
  end.wait
142
-
143
- # Re-raise any error outside the async block
144
- raise error if error
145
-
146
- result
147
132
  end
148
133
  end
149
134
 
150
- # Helper method to execute the transaction
151
- def execute_transaction(mode, db:, timeout:, metadata:)
152
- tx = begin_transaction(db: db,
153
- access_mode: mode,
154
- tx_timeout: timeout,
155
- tx_metadata: metadata)
156
-
157
- result = yield tx # your block runs here
158
- tx.commit # happy path
159
- result
160
- rescue StandardError => e # any error rollback wrap
161
- begin
162
- tx.rollback
163
- rescue StandardError => rollback_error
164
- # Log rollback error but continue with the original error
165
- puts "Error during rollback: #{rollback_error.message}" if ENV['DEBUG']
166
- end
135
+ # Asynchronously execute a block of code within a transaction.
136
+ # This method is asynchronous and will return an `Async::Task` that will complete when the transaction is finished.
137
+ #
138
+ # @param mode [Symbol] The access mode (:read or :write).
139
+ # @param db [String] The database name to run the transaction against.
140
+ # @param timeout [Integer] Transaction timeout in milliseconds.
141
+ # @param metadata [Hash] Transaction metadata to send to the server.
142
+ # @yield [tx] The transaction to use for queries.
143
+ # @return [Async::Task] A task that will complete with the result of the block.
144
+ def async_run_transaction(mode = :write, db: nil, timeout: nil, metadata: nil, &block)
145
+ # Ensure we are in an async task, otherwise the behavior is undefined.
146
+ raise 'Cannot run an async transaction outside of an Async task' unless Async::Task.current?
167
147
 
168
- # Preserve the original error
169
- raise ActiveCypher::TransactionError, e.message
148
+ Async do
149
+ _execute_transaction_block(mode, db, timeout, metadata, &block)
150
+ end
170
151
  end
171
152
 
172
153
  def write_transaction(db: nil, timeout: nil, metadata: nil, &)
@@ -177,6 +158,55 @@ module ActiveCypher
177
158
  run_transaction(:read, db: db, timeout: timeout, metadata: metadata, &)
178
159
  end
179
160
 
161
+ def async_write_transaction(db: nil, timeout: nil, metadata: nil, &block)
162
+ async_run_transaction(:write, db: db, timeout: timeout, metadata: metadata, &block)
163
+ end
164
+
165
+ def async_read_transaction(db: nil, timeout: nil, metadata: nil, &block)
166
+ async_run_transaction(:read, db: db, timeout: timeout, metadata: metadata, &block)
167
+ end
168
+
169
+ # Close the session and any active transaction.
170
+ def close
171
+ instrument('session.close') do
172
+ # If there's an active transaction, try to roll it back
173
+ @current_transaction&.rollback if @current_transaction&.active?
174
+
175
+ # Mark current transaction as complete
176
+ complete_transaction(@current_transaction) if @current_transaction
177
+ end
178
+ end
179
+
180
+ private
181
+
182
+ def _execute_transaction_block(mode, db, timeout, metadata, &block)
183
+ tx = begin_transaction(db: db, access_mode: mode, tx_timeout: timeout, tx_metadata: metadata)
184
+ begin
185
+ result = block.call(tx)
186
+ tx.commit
187
+ result
188
+ rescue StandardError => e
189
+ # On any error, rollback the transaction and re-raise the original exception
190
+ begin
191
+ tx.rollback
192
+ rescue StandardError => rollback_error
193
+ # Log rollback error but continue with the original error
194
+ puts "Error during rollback: #{rollback_error.message}" if ENV['DEBUG']
195
+ end
196
+
197
+ # Reset the connection to ensure it's in a clean state for the next transaction
198
+ begin
199
+ @connection.reset!
200
+ rescue StandardError => reset_error
201
+ # If reset fails, the connection will be marked non-viable by the pool
202
+ puts "Error during connection reset: #{reset_error.message}" if ENV['DEBUG']
203
+ end
204
+
205
+ # Wrap the error in TransactionError to maintain compatibility
206
+ raise ActiveCypher::TransactionError, e.message
207
+ end
208
+ end
209
+
180
210
  # Access the current bookmarks for this session.
181
211
  def bookmarks
182
212
  @bookmarks || []
@@ -197,17 +227,6 @@ module ActiveCypher
197
227
  @connection.reset!
198
228
  end
199
229
  end
200
-
201
- # Close the session and any active transaction.
202
- def close
203
- instrument('session.close') do
204
- # If there's an active transaction, try to roll it back
205
- @current_transaction&.rollback if @current_transaction&.active?
206
-
207
- # Mark current transaction as complete
208
- complete_transaction(@current_transaction) if @current_transaction
209
- end
210
- end
211
230
  end
212
231
  end
213
232
  end
@@ -49,6 +49,14 @@ module ActiveCypher
49
49
  end
50
50
  end
51
51
 
52
+ # Hydrates attributes from a database record
53
+ # @param record [Hash] The raw record from the database
54
+ # @param node_alias [Symbol] The alias used for the node in the query
55
+ # @return [Hash] The hydrated attributes
56
+ def hydrate_record(record, node_alias)
57
+ raise NotImplementedError, "#{self.class} must implement #hydrate_record"
58
+ end
59
+
52
60
  # Get current adapter type for ID handling
53
61
  # Helper for generating ID-related Cypher functions that are database-specific
54
62
  module CypherFunction
@@ -12,6 +12,13 @@ module ActiveCypher
12
12
  include Instrumentation
13
13
  attr_reader :connection
14
14
 
15
+ # Returns the raw Bolt connection object
16
+ # This is useful for accessing low-level connection methods like
17
+ # read_transaction, write_transaction, async_read_transaction, etc.
18
+ def raw_connection
19
+ @connection
20
+ end
21
+
15
22
  # Establish a connection if not already active.
16
23
  # This includes auth token prep, URI parsing, and quiet suffering.
17
24
  def connect
@@ -108,6 +108,30 @@ module ActiveCypher
108
108
  metadata.compact
109
109
  end
110
110
 
111
+ # Hydrates attributes from a Memgraph record
112
+ # @param record [Hash] The raw record from Memgraph
113
+ # @param node_alias [Symbol] The alias used for the node in the query
114
+ # @return [Hash] The hydrated attributes
115
+ def hydrate_record(record, node_alias)
116
+ attrs = {}
117
+ node_data = record[node_alias] || record[node_alias.to_s]
118
+
119
+ if node_data.is_a?(Array) && node_data.length >= 2
120
+ properties_container = node_data[1]
121
+ if properties_container.is_a?(Array) && properties_container.length >= 3
122
+ properties = properties_container[2]
123
+ properties.each { |k, v| attrs[k.to_sym] = v } if properties.is_a?(Hash)
124
+ end
125
+ elsif node_data.is_a?(Hash)
126
+ node_data.each { |k, v| attrs[k.to_sym] = v }
127
+ elsif node_data.respond_to?(:properties)
128
+ attrs = node_data.properties.symbolize_keys
129
+ end
130
+
131
+ attrs[:internal_id] = record[:internal_id] || record['internal_id']
132
+ attrs
133
+ end
134
+
111
135
  protected
112
136
 
113
137
  def parse_schema(rows)
@@ -130,6 +130,30 @@ 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
+
133
157
  module Persistence
134
158
  include PersistenceMethods
135
159
  module_function :create_record, :update_record, :destroy_record
@@ -39,9 +39,7 @@ module ActiveCypher
39
39
  adapter = connection.id_handler
40
40
  label_string = labels.map { |l| ":#{l}" }.join
41
41
 
42
- # Handle ID format based on adapter's preferred flavor of existential crisis
43
- # Neo4j insists on string IDs like "4:uuid:wtf" because simple integers are for peasants
44
- # Memgraph keeps it real with numeric IDs because it doesn't need to prove anything
42
+ # Handle ID format based on adapter type
45
43
  formatted_id = if adapter.id_function == 'elementId'
46
44
  internal_db_id.to_s # String for Neo4j
47
45
  else
@@ -59,7 +57,7 @@ module ActiveCypher
59
57
  record = result.first
60
58
 
61
59
  if record
62
- attrs = _hydrate_attributes_from_memgraph_record(record, node_alias)
60
+ attrs = connection.hydrate_record(record, node_alias)
63
61
  return instantiate(attrs)
64
62
  end
65
63
 
@@ -97,28 +95,6 @@ module ActiveCypher
97
95
  # @return [Object] The new, possibly persisted record
98
96
  # Because sometimes you just want to live dangerously.
99
97
  def create(attrs = {}) = new(attrs).tap(&:save)
100
-
101
- private
102
-
103
- def _hydrate_attributes_from_memgraph_record(record, node_alias)
104
- attrs = {}
105
- node_data = record[node_alias] || record[node_alias.to_s]
106
-
107
- if node_data.is_a?(Array) && node_data.length >= 2
108
- properties_container = node_data[1]
109
- if properties_container.is_a?(Array) && properties_container.length >= 3
110
- properties = properties_container[2]
111
- properties.each { |k, v| attrs[k.to_sym] = v } if properties.is_a?(Hash)
112
- end
113
- elsif node_data.is_a?(Hash)
114
- node_data.each { |k, v| attrs[k.to_sym] = v }
115
- elsif node_data.respond_to?(:properties)
116
- attrs = node_data.properties.symbolize_keys
117
- end
118
-
119
- attrs[:internal_id] = record[:internal_id] || record['internal_id']
120
- attrs
121
- end
122
98
  end
123
99
  end
124
100
  end
@@ -366,16 +366,14 @@ module ActiveCypher
366
366
 
367
367
  props = attributes.except('internal_id').compact
368
368
  rel_ty = self.class.relationship_type
369
- arrow = '->' # outgoing by default
370
-
371
369
  adapter = self.class.connection.id_handler
372
- parts = []
373
370
 
374
371
  # Build the Cypher query based on the adapter
375
372
  id_clause = adapter.with_direct_node_ids(from_node.internal_id, to_node.internal_id)
373
+ parts = []
376
374
  parts << "MATCH (p), (h) WHERE #{id_clause}"
377
- parts << "CREATE (p)-[r:#{rel_ty}]#{arrow}(h)"
378
- parts << 'SET r += $props' unless props.empty? # only if we have props
375
+ parts << "CREATE (p)-[r:#{rel_ty}]->(h)"
376
+ parts << 'SET r += $props' unless props.empty?
379
377
  parts << "RETURN #{adapter.return_id}"
380
378
 
381
379
  cypher = parts.join(' ')
@@ -383,7 +381,6 @@ module ActiveCypher
383
381
 
384
382
  # Execute Cypher query
385
383
  result = self.class.connection.execute_cypher(cypher, params, 'Create Relationship')
386
-
387
384
  row = result.first
388
385
 
389
386
  # Try different ways to access the ID
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveCypher
4
- VERSION = '0.10.4'
4
+ VERSION = '0.11.0'
5
5
 
6
6
  def self.gem_version
7
7
  Gem::Version.new VERSION
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.10.4
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -265,7 +265,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
265
265
  - !ruby/object:Gem::Version
266
266
  version: '0'
267
267
  requirements: []
268
- rubygems_version: 3.6.9
268
+ rubygems_version: 3.6.7
269
269
  specification_version: 4
270
270
  summary: OpenCypher Adapter ala ActiveRecord
271
271
  test_files: []