activecypher 0.10.4 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45703ee8a46ca012c9c4e6bd82708f3846bdea0fae15c4fb7fbff7a46d0fb3b3
4
- data.tar.gz: 93e134bd2615288003c16c94159344b48023d3d593f45e47441483a5be3c02b1
3
+ metadata.gz: a39f60243be9073a1c3e2f05e1239239de9412d95184a321d22c4f582b0cdbdb
4
+ data.tar.gz: f125c98717432f24b3e0e28e4066f962cc2cb46bbab556bda52a44c3f4f12e7c
5
5
  SHA512:
6
- metadata.gz: 9232c48290ea388ad7c49b4a3dfb4ffffc5f4493c33fa3000ad7936c40730f5c6b106a9190b61a52576d80f09c4632a903696f0c0c683332fd6c6325a2c9d7af
7
- data.tar.gz: d48c94cf08b6aa3d1a7a9659b13fdde62fdb0ca444a281e97ccea3e28646bc0520702e3f90df59a0e9b2650b48a65e6170cedea896e2a7402901d6df7a5615dc
6
+ metadata.gz: 3bea961b864932446e0c667c7f88c9a428204f62089696b65d32dc62c4297e10e7d86e1f2ce67db8eb32e3a771944d882840b5d416345eed3296a3b23dec1d09
7
+ data.tar.gz: 1803663082f2fa06c7af69af355d0bd05ad78901c217c8b5f2aa2486b3adc009a6784d11b09731b1bd88d0b533e12420c13b11254b4b0852de5636b1e0d530f3
@@ -34,7 +34,6 @@ module ActiveCypher
34
34
  include Model::Persistence
35
35
  include Model::Destruction
36
36
  include Model::Countable
37
- include Model::Inspectable
38
37
 
39
38
  class << self
40
39
  # Attempts to retrieve a connection from the handler.
@@ -61,6 +60,24 @@ module ActiveCypher
61
60
  end
62
61
  end
63
62
 
63
+ # Custom object inspection method for pretty-printing a compact,
64
+ # single-line summary of the object. Output examples:
65
+ #
66
+ # #<UserNode id="26" name="Alice" age=34> => persisted object
67
+ # #<UserNode (new) name="Bob"> => object not yet saved
68
+ #
69
+ def inspect
70
+ # Put 'internal_id' first like it's the main character (even if it's nil)
71
+ ordered = attributes.dup
72
+ ordered = ordered.slice('internal_id').merge(ordered.except('internal_id'))
73
+
74
+ # Turn each attr into "key: value" because we humans fear raw hashes
75
+ parts = ordered.map { |k, v| "#{k}: #{v.inspect}" }
76
+
77
+ # Wrap it all up in a fake-sane object string, so you can pretend your data is organized.
78
+ "#<#{self.class} #{parts.join(', ')}>"
79
+ end
80
+
64
81
  # Because Rails needs to feel included, too.
65
82
  ActiveSupport.run_load_hooks(:active_cypher, self)
66
83
  end
@@ -133,7 +133,7 @@ module ActiveCypher
133
133
  #
134
134
  # @note The digital equivalent of ghosting.
135
135
  def close
136
- @socket&.close if connected?
136
+ @socket.close if connected?
137
137
  rescue IOError
138
138
  ensure
139
139
  @socket = nil
@@ -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
@@ -47,11 +47,7 @@ module ActiveCypher
47
47
  # Check if connection is viable before using it
48
48
  unless conn.viable?
49
49
  # Create a fresh connection, because hope springs eternal
50
- begin
51
- conn.close
52
- rescue StandardError
53
- nil
54
- end
50
+ conn.close
55
51
  conn = build_connection
56
52
  end
57
53
 
@@ -64,11 +60,7 @@ module ActiveCypher
64
60
  # Check if connection is viable before using it
65
61
  unless conn.viable?
66
62
  # Create a fresh connection, because why not
67
- begin
68
- conn.close
69
- rescue StandardError
70
- nil
71
- end
63
+ conn.close
72
64
  conn = build_connection
73
65
  end
74
66
 
@@ -88,8 +80,6 @@ module ActiveCypher
88
80
  def verify_connectivity
89
81
  with_session { |s| s.run('RETURN 1') }
90
82
  true
91
- rescue StandardError
92
- false
93
83
  end
94
84
 
95
85
  # Closes the connection pool. Because sometimes you just need to let go.
@@ -119,11 +109,7 @@ module ActiveCypher
119
109
  begin
120
110
  connection.connect
121
111
  rescue StandardError => e
122
- begin
123
- connection.close
124
- rescue StandardError
125
- nil
126
- end
112
+ connection.close
127
113
  raise e
128
114
  end
129
115