activecypher 0.14.2 → 0.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/active_cypher/associations/collection_proxy.rb +5 -2
- data/lib/active_cypher/associations.rb +22 -32
- data/lib/active_cypher/base.rb +14 -2
- data/lib/active_cypher/bolt/connection.rb +16 -102
- data/lib/active_cypher/bolt/driver.rb +4 -4
- data/lib/active_cypher/bolt/messaging.rb +32 -104
- data/lib/active_cypher/bolt/packstream.rb +15 -23
- data/lib/active_cypher/connection_adapters/abstract_adapter.rb +17 -1
- data/lib/active_cypher/connection_adapters/abstract_bolt_adapter.rb +21 -23
- data/lib/active_cypher/connection_adapters/memgraph_adapter.rb +4 -24
- data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +0 -24
- data/lib/active_cypher/migration.rb +2 -3
- data/lib/active_cypher/model/connection_owner.rb +1 -2
- data/lib/active_cypher/model/core.rb +8 -0
- data/lib/active_cypher/railtie.rb +2 -2
- data/lib/active_cypher/relation.rb +3 -2
- data/lib/active_cypher/relationship.rb +19 -15
- data/lib/active_cypher/version.rb +1 -1
- data/lib/cyrel/clause/create.rb +1 -5
- data/lib/cyrel/clause/match.rb +1 -7
- data/lib/cyrel/clause/merge.rb +1 -5
- data/lib/cyrel/expression/exists.rb +1 -4
- data/lib/cyrel/expression/pattern_comprehension.rb +1 -4
- data/lib/cyrel/pattern/node.rb +1 -1
- data/lib/cyrel/pattern.rb +10 -0
- data/lib/cyrel/query.rb +63 -117
- data/lib/cyrel.rb +14 -35
- 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: dc6fb1ecc443117671c36c2b287b44d0059d706c8543ef113915fe0c67e8063d
|
|
4
|
+
data.tar.gz: 7f7a6e862f2c146dc19e89827e2b0ab2333bcc58f801b07a78e3e0c89f17fc6f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6cd38267fc15f40e53c9d9d79d11fe52138cc20aa93290043e2aae7834616b6d78afb952a7c1c1ad6baf8e08531170618e43f297d70074c9db806928cd89b807
|
|
7
|
+
data.tar.gz: c2b0186231f35c838a56272ff6222f64af8a7be53891c8ed90260e297acdccef42992f28b65729ff61410172570cdcd92209c3c817d920d30341cc88906be9ff
|
|
@@ -37,12 +37,15 @@ module ActiveCypher
|
|
|
37
37
|
load_target unless @records
|
|
38
38
|
@records.each(&)
|
|
39
39
|
end
|
|
40
|
-
|
|
40
|
+
|
|
41
|
+
def to_a
|
|
42
|
+
each.to_a
|
|
43
|
+
end
|
|
41
44
|
|
|
42
45
|
# Returns the size, because counting things is the only certainty in life.
|
|
43
46
|
#
|
|
44
47
|
# @return [Integer] The number of records in the collection
|
|
45
|
-
def size
|
|
48
|
+
def size = load_target.size
|
|
46
49
|
alias length size
|
|
47
50
|
|
|
48
51
|
# Fully refresh from the database.
|
|
@@ -114,14 +114,20 @@ module ActiveCypher
|
|
|
114
114
|
|
|
115
115
|
# Resolve the target node class
|
|
116
116
|
target_class = target_class_name.constantize
|
|
117
|
-
a_alias = :start
|
|
118
|
-
b_alias = :target
|
|
119
117
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
owner_alias = :start
|
|
119
|
+
related_alias = :target
|
|
120
|
+
|
|
121
|
+
# owner = the node we're querying from (self)
|
|
122
|
+
# related = the node we want to fetch
|
|
123
|
+
owner_node = Cyrel::Pattern::Node.new(owner_alias, labels: self.class.label_name)
|
|
124
|
+
related_node = Cyrel::Pattern::Node.new(related_alias, labels: target_class.label_name)
|
|
123
125
|
|
|
124
|
-
#
|
|
126
|
+
# The relationship token renders the arrow direction itself:
|
|
127
|
+
# :out → -[:TYPE]-> e.g. (start:Person)-[:ENJOYS]->(target:Activity)
|
|
128
|
+
# :in → <-[:TYPE]- e.g. (start:Activity)<-[:ENJOYS]-(target:Person)
|
|
129
|
+
# :both → -[:TYPE]- e.g. (start)-[:TYPE]-(target)
|
|
130
|
+
# Node order is always [owner, rel, related] — never swapped.
|
|
125
131
|
rel_direction = case direction
|
|
126
132
|
when :out then Cyrel::Direction::OUT
|
|
127
133
|
when :in then Cyrel::Direction::IN
|
|
@@ -133,17 +139,13 @@ module ActiveCypher
|
|
|
133
139
|
direction: rel_direction
|
|
134
140
|
)
|
|
135
141
|
|
|
136
|
-
|
|
137
|
-
path = case direction
|
|
138
|
-
when :in then Cyrel::Pattern::Path.new([b_node, rel_node, a_node])
|
|
139
|
-
else Cyrel::Pattern::Path.new([a_node, rel_node, b_node])
|
|
140
|
-
end
|
|
142
|
+
path = Cyrel::Pattern::Path.new([owner_node, rel_node, related_node])
|
|
141
143
|
|
|
142
144
|
# Compose query MATCH – WHERE – RETURN
|
|
143
145
|
query = Cyrel::Query.new
|
|
144
146
|
.match(path)
|
|
145
|
-
.where(Cyrel.node_id(
|
|
146
|
-
.return_(
|
|
147
|
+
.where(Cyrel.node_id(owner_alias).eq(internal_id))
|
|
148
|
+
.return_(related_alias)
|
|
147
149
|
|
|
148
150
|
base_relation = Relation.new(target_class, query)
|
|
149
151
|
|
|
@@ -283,6 +285,12 @@ module ActiveCypher
|
|
|
283
285
|
instance_variable_set(instance_var, associate)
|
|
284
286
|
end
|
|
285
287
|
|
|
288
|
+
define_build_and_create_methods(name, target_class_name)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Defines build_<name> and create_<name> for singular associations
|
|
292
|
+
# (shared by belongs_to and has_one).
|
|
293
|
+
def define_build_and_create_methods(name, target_class_name)
|
|
286
294
|
# Define build method (e.g., build_author(name: "New Author"))
|
|
287
295
|
define_method("build_#{name}") do |attributes = {}|
|
|
288
296
|
target_class = target_class_name.constantize
|
|
@@ -429,25 +437,7 @@ module ActiveCypher
|
|
|
429
437
|
instance_variable_set(instance_var, associate)
|
|
430
438
|
end
|
|
431
439
|
|
|
432
|
-
|
|
433
|
-
define_method("build_#{name}") do |attributes = {}|
|
|
434
|
-
target_class = target_class_name.constantize
|
|
435
|
-
# TODO: Potentially set the inverse association reference here
|
|
436
|
-
# For now, just instantiate the target class
|
|
437
|
-
target_class.new(attributes)
|
|
438
|
-
end
|
|
439
|
-
|
|
440
|
-
# Define create method (e.g., create_profile(data: {...}))
|
|
441
|
-
define_method("create_#{name}") do |attributes = {}|
|
|
442
|
-
# Build the instance
|
|
443
|
-
instance = public_send("build_#{name}", attributes)
|
|
444
|
-
# Save the instance
|
|
445
|
-
instance.save
|
|
446
|
-
# If save is successful, associate it using the = method
|
|
447
|
-
public_send("#{name}=", instance) if instance.persisted?
|
|
448
|
-
# Return the instance
|
|
449
|
-
instance
|
|
450
|
-
end
|
|
440
|
+
define_build_and_create_methods(name, target_class_name)
|
|
451
441
|
end
|
|
452
442
|
end
|
|
453
443
|
|
data/lib/active_cypher/base.rb
CHANGED
|
@@ -13,7 +13,7 @@ module ActiveCypher
|
|
|
13
13
|
class Base
|
|
14
14
|
# @!attribute [rw] connects_to_mappings
|
|
15
15
|
# @return [Hash] Because every base class needs a mapping it will never use directly.
|
|
16
|
-
class_attribute :connects_to_mappings, default: {}
|
|
16
|
+
class_attribute :connects_to_mappings, default: { reading: :primary, writing: :primary }
|
|
17
17
|
|
|
18
18
|
# Rails/ActiveModel foundations
|
|
19
19
|
include Logging
|
|
@@ -30,6 +30,7 @@ module ActiveCypher
|
|
|
30
30
|
include Model::Querying
|
|
31
31
|
include Model::Abstract
|
|
32
32
|
include Model::Attributes
|
|
33
|
+
include Model::ConnectionHandling
|
|
33
34
|
include Model::ConnectionOwner
|
|
34
35
|
include Model::Persistence
|
|
35
36
|
include Model::Destruction
|
|
@@ -44,7 +45,7 @@ module ActiveCypher
|
|
|
44
45
|
# Determine the current role (e.g., :writing, :reading)
|
|
45
46
|
# ActiveCypher::RuntimeRegistry.current_role defaults to :writing
|
|
46
47
|
# Only use db_key for pool lookup
|
|
47
|
-
mapping = connects_to_mappings
|
|
48
|
+
mapping = connects_to_mappings
|
|
48
49
|
role = ActiveCypher::RuntimeRegistry.current_role || :writing
|
|
49
50
|
# Debug guardrails removed in release code; rely on role/shard registry.
|
|
50
51
|
|
|
@@ -82,6 +83,17 @@ module ActiveCypher
|
|
|
82
83
|
"#<#{self.class} #{parts.join(', ')}>"
|
|
83
84
|
end
|
|
84
85
|
|
|
86
|
+
def internal_id
|
|
87
|
+
working_id = super
|
|
88
|
+
return working_id if working_id.nil?
|
|
89
|
+
|
|
90
|
+
if connection.respond_to?(:id_type_conversion)
|
|
91
|
+
connection.id_type_conversion(working_id)
|
|
92
|
+
else
|
|
93
|
+
working_id
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
85
97
|
# Because Rails needs to feel included, too.
|
|
86
98
|
ActiveSupport.run_load_hooks(:active_cypher, self)
|
|
87
99
|
end
|
|
@@ -485,7 +485,6 @@ module ActiveCypher
|
|
|
485
485
|
session(database: db).write_transaction(db: db, timeout: timeout, metadata: metadata, &)
|
|
486
486
|
end
|
|
487
487
|
|
|
488
|
-
|
|
489
488
|
# ────────────────────────────────────────────────────────────────────
|
|
490
489
|
# HEALTH AND VERSION DETECTION METHODS
|
|
491
490
|
# ────────────────────────────────────────────────────────────────────
|
|
@@ -514,21 +513,17 @@ module ActiveCypher
|
|
|
514
513
|
def health_check
|
|
515
514
|
return { healthy: false, response_time_ms: nil, details: 'Not connected' } unless connected?
|
|
516
515
|
|
|
517
|
-
|
|
516
|
+
nil
|
|
518
517
|
|
|
519
518
|
begin
|
|
520
|
-
|
|
521
|
-
case database_type
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
|
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
|
|
709
|
+
def perform_health_check_query(query)
|
|
796
710
|
start_time = Time.now
|
|
797
711
|
|
|
798
712
|
begin
|
|
799
|
-
write_message(Messaging::Run.new(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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, &
|
|
43
|
+
def with_session(**kw, &)
|
|
44
44
|
Sync do
|
|
45
|
-
_acquire_session(**kw, &
|
|
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, &
|
|
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, &
|
|
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
|
-
#
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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 <
|
|
165
|
+
class Discard < MetadataMessage
|
|
170
166
|
SIGNATURE = 0x2F
|
|
171
167
|
|
|
172
168
|
# metadata: { n: <N>, qid: <QID> }, where n = -1 means all
|
|
173
|
-
def
|
|
174
|
-
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|