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 +4 -4
- data/lib/active_cypher/bolt/connection.rb +420 -15
- data/lib/active_cypher/bolt/session.rb +70 -51
- data/lib/active_cypher/connection_adapters/abstract_adapter.rb +8 -0
- data/lib/active_cypher/connection_adapters/abstract_bolt_adapter.rb +7 -0
- data/lib/active_cypher/connection_adapters/memgraph_adapter.rb +24 -0
- data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +24 -0
- data/lib/active_cypher/model/querying.rb +2 -26
- data/lib/active_cypher/relationship.rb +3 -6
- data/lib/active_cypher/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2396c4b661d5d743f6c35c7921d020f096531f543c7df9b99e05bddf84c9a2db
|
4
|
+
data.tar.gz: 51e407abc0ac00aaa315fd8acdf489fad8a8d8ae3a46702df4b97c8347ea3fbd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
#
|
234
|
-
|
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
|
-
|
236
|
+
if health[:healthy]
|
243
237
|
true
|
244
|
-
|
245
|
-
# If
|
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
|
126
|
-
|
127
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
#
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
169
|
-
|
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
|
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 =
|
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}]
|
378
|
-
parts << 'SET r += $props' unless props.empty?
|
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
|
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.
|
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.
|
268
|
+
rubygems_version: 3.6.7
|
269
269
|
specification_version: 4
|
270
270
|
summary: OpenCypher Adapter ala ActiveRecord
|
271
271
|
test_files: []
|