activecypher 0.0.0 → 0.3.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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/lib/active_cypher/associations/collection_proxy.rb +144 -0
  3. data/lib/active_cypher/associations.rb +537 -0
  4. data/lib/active_cypher/base.rb +47 -0
  5. data/lib/active_cypher/bolt/connection.rb +525 -0
  6. data/lib/active_cypher/bolt/driver.rb +144 -0
  7. data/lib/active_cypher/bolt/handlers.rb +10 -0
  8. data/lib/active_cypher/bolt/message_reader.rb +100 -0
  9. data/lib/active_cypher/bolt/message_writer.rb +53 -0
  10. data/lib/active_cypher/bolt/messaging.rb +307 -0
  11. data/lib/active_cypher/bolt/packstream.rb +319 -0
  12. data/lib/active_cypher/bolt/result.rb +82 -0
  13. data/lib/active_cypher/bolt/session.rb +201 -0
  14. data/lib/active_cypher/bolt/transaction.rb +211 -0
  15. data/lib/active_cypher/bolt/version_encoding.rb +41 -0
  16. data/lib/active_cypher/bolt.rb +7 -0
  17. data/lib/active_cypher/connection_adapters/abstract_adapter.rb +75 -0
  18. data/lib/active_cypher/connection_adapters/abstract_bolt_adapter.rb +178 -0
  19. data/lib/active_cypher/connection_adapters/memgraph_adapter.rb +44 -0
  20. data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +58 -0
  21. data/lib/active_cypher/connection_factory.rb +130 -0
  22. data/lib/active_cypher/connection_handler.rb +9 -0
  23. data/lib/active_cypher/connection_pool.rb +123 -0
  24. data/lib/active_cypher/connection_url_resolver.rb +137 -0
  25. data/lib/active_cypher/cypher_config.rb +61 -0
  26. data/lib/active_cypher/generators/install_generator.rb +23 -0
  27. data/lib/active_cypher/generators/node_generator.rb +32 -0
  28. data/lib/active_cypher/generators/relationship_generator.rb +33 -0
  29. data/lib/active_cypher/generators/templates/application_graph_node.rb +5 -0
  30. data/lib/active_cypher/generators/templates/application_graph_relationship.rb +5 -0
  31. data/lib/active_cypher/generators/templates/cypher_databases.yml +16 -0
  32. data/lib/active_cypher/generators/templates/node.rb.erb +10 -0
  33. data/lib/active_cypher/generators/templates/relationship.rb.erb +11 -0
  34. data/lib/active_cypher/logging.rb +44 -0
  35. data/lib/active_cypher/model/abstract.rb +87 -0
  36. data/lib/active_cypher/model/attributes.rb +24 -0
  37. data/lib/active_cypher/model/callbacks.rb +44 -0
  38. data/lib/active_cypher/model/connection_handling.rb +76 -0
  39. data/lib/active_cypher/model/connection_owner.rb +50 -0
  40. data/lib/active_cypher/model/core.rb +45 -0
  41. data/lib/active_cypher/model/countable.rb +30 -0
  42. data/lib/active_cypher/model/destruction.rb +49 -0
  43. data/lib/active_cypher/model/inspectable.rb +28 -0
  44. data/lib/active_cypher/model/persistence.rb +182 -0
  45. data/lib/active_cypher/model/querying.rb +67 -0
  46. data/lib/active_cypher/railtie.rb +34 -0
  47. data/lib/active_cypher/relation.rb +190 -0
  48. data/lib/active_cypher/relationship.rb +233 -0
  49. data/lib/active_cypher/runtime_registry.rb +8 -0
  50. data/lib/active_cypher/scoping.rb +97 -0
  51. data/lib/active_cypher/utils/logger.rb +100 -0
  52. data/lib/active_cypher/version.rb +5 -0
  53. data/lib/activecypher.rb +108 -0
  54. data/lib/cyrel/call_procedure.rb +29 -0
  55. data/lib/cyrel/clause/call.rb +46 -0
  56. data/lib/cyrel/clause/call_subquery.rb +40 -0
  57. data/lib/cyrel/clause/create.rb +33 -0
  58. data/lib/cyrel/clause/delete.rb +41 -0
  59. data/lib/cyrel/clause/limit.rb +33 -0
  60. data/lib/cyrel/clause/match.rb +40 -0
  61. data/lib/cyrel/clause/merge.rb +34 -0
  62. data/lib/cyrel/clause/order_by.rb +78 -0
  63. data/lib/cyrel/clause/remove.rb +75 -0
  64. data/lib/cyrel/clause/return.rb +90 -0
  65. data/lib/cyrel/clause/set.rb +97 -0
  66. data/lib/cyrel/clause/skip.rb +34 -0
  67. data/lib/cyrel/clause/where.rb +42 -0
  68. data/lib/cyrel/clause/with.rb +94 -0
  69. data/lib/cyrel/clause.rb +25 -0
  70. data/lib/cyrel/direction.rb +18 -0
  71. data/lib/cyrel/expression/alias.rb +27 -0
  72. data/lib/cyrel/expression/base.rb +101 -0
  73. data/lib/cyrel/expression/case.rb +45 -0
  74. data/lib/cyrel/expression/comparison.rb +60 -0
  75. data/lib/cyrel/expression/exists.rb +42 -0
  76. data/lib/cyrel/expression/function_call.rb +57 -0
  77. data/lib/cyrel/expression/literal.rb +33 -0
  78. data/lib/cyrel/expression/logical.rb +38 -0
  79. data/lib/cyrel/expression/operator.rb +27 -0
  80. data/lib/cyrel/expression/pattern_comprehension.rb +44 -0
  81. data/lib/cyrel/expression/property_access.rb +25 -0
  82. data/lib/cyrel/expression.rb +56 -0
  83. data/lib/cyrel/functions.rb +116 -0
  84. data/lib/cyrel/node.rb +397 -0
  85. data/lib/cyrel/parameterizable.rb +20 -0
  86. data/lib/cyrel/pattern/node.rb +66 -0
  87. data/lib/cyrel/pattern/path.rb +41 -0
  88. data/lib/cyrel/pattern/relationship.rb +74 -0
  89. data/lib/cyrel/pattern.rb +8 -0
  90. data/lib/cyrel/query.rb +497 -0
  91. data/lib/cyrel/return_only.rb +26 -0
  92. data/lib/cyrel/types/hash_type.rb +22 -0
  93. data/lib/cyrel/types/symbol_type.rb +13 -0
  94. data/lib/cyrel.rb +72 -0
  95. data/sig/activecypher.rbs +4 -0
  96. metadata +172 -10
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+
5
+ module ActiveCypher
6
+ module Bolt
7
+ # Handles decoding chunked Packstream data into Bolt messages for Bolt v5.x.
8
+ class MessageReader
9
+ MAX_CHUNK_SIZE = 65_535 # Maximum size for a single chunk (unsigned 16-bit int)
10
+ READ_TIMEOUT = 15 # Seconds to wait for a read operation
11
+
12
+ def initialize(io)
13
+ @io = io
14
+ @buffer = StringIO.new(+'', 'rb+') # Internal buffer for chunked data
15
+ end
16
+
17
+ # Reads and decodes the next Bolt message from the stream.
18
+ # Handles Bolt's chunking mechanism.
19
+ #
20
+ # @return [Messaging::Message] The decoded message object.
21
+ # @raise [ProtocolError] If decoding fails or an unknown message type is received.
22
+ # @raise [ConnectionError] If the connection is lost during reading.
23
+ def read_message
24
+ message_bytes = read_message_chunks
25
+ return nil if message_bytes.nil? || message_bytes.empty?
26
+
27
+ unpacker = Packstream::Unpacker.new(StringIO.new(message_bytes, 'rb'))
28
+ signature, fields = unpacker.unpack
29
+
30
+ klass = find_message_class(signature) or
31
+ raise ProtocolError, "Unknown message signature 0x#{signature.to_s(16)}"
32
+
33
+ klass.new(*fields)
34
+ rescue EOFError => e
35
+ raise ConnectionError, "Connection closed while reading message: #{e.message}"
36
+ rescue ConnectionError
37
+ raise
38
+ rescue StandardError => e
39
+ raise ProtocolError, "Failed to decode message: #{e.class} - #{e.message}"
40
+ end
41
+
42
+ private
43
+
44
+ def read_raw_from_io(n)
45
+ Async::Task.current.with_timeout(READ_TIMEOUT) do
46
+ data = @io.read_exactly(n)
47
+ raise EOFError, 'Connection closed during read' unless data
48
+
49
+ return data
50
+ end
51
+ rescue Async::TimeoutError
52
+ raise ConnectionError, "Read operation timed out after #{READ_TIMEOUT}s"
53
+ rescue Errno::ECONNRESET, Errno::EPIPE, IOError, EOFError => e
54
+ raise ConnectionError, "Connection lost: #{e.message}"
55
+ end
56
+
57
+ # Reads message chunks from the IO stream until a zero chunk is found.
58
+ # @return [String] The concatenated bytes of the message.
59
+ def read_message_chunks
60
+ @buffer.rewind
61
+ @buffer.truncate(0)
62
+
63
+ loop do
64
+ chunk_size_bytes = read_raw_from_io(2)
65
+ chunk_size = chunk_size_bytes.unpack1('n')
66
+
67
+ break if chunk_size.zero?
68
+ raise ProtocolError, "Chunk too large (#{chunk_size})" if chunk_size > MAX_CHUNK_SIZE
69
+
70
+ @buffer.write(read_raw_from_io(chunk_size))
71
+ end
72
+
73
+ @buffer.string
74
+ end
75
+
76
+ # Finds the message class corresponding to a signature byte.
77
+ def find_message_class(signature)
78
+ case signature
79
+ when Messaging::Success::SIGNATURE then Messaging::Success
80
+ when Messaging::Failure::SIGNATURE then Messaging::Failure
81
+ when Messaging::Ignored::SIGNATURE then Messaging::Ignored
82
+ when Messaging::Hello::SIGNATURE then Messaging::Hello # Technically shouldn't receive HELLO, its old stuff
83
+ when Messaging::Run::SIGNATURE then Messaging::Run # Shouldn't receive RUN either
84
+ when Messaging::Pull::SIGNATURE then Messaging::Pull # Shouldn't receive PULL
85
+ when Messaging::Discard::SIGNATURE then Messaging::Discard # Shouldn't receive DISCARD
86
+ when Messaging::Record::SIGNATURE then Messaging::Record
87
+ when Messaging::Begin::SIGNATURE then Messaging::Begin # Shouldn't receive BEGIN
88
+ when Messaging::Commit::SIGNATURE then Messaging::Commit # Shouldn't receive COMMIT
89
+ when Messaging::Rollback::SIGNATURE then Messaging::Rollback # Shouldn't receive ROLLBACK
90
+ when Messaging::Goodbye::SIGNATURE then Messaging::Goodbye
91
+ when Messaging::Logon::SIGNATURE then Messaging::Logon
92
+ when Messaging::Logoff::SIGNATURE then Messaging::Logoff
93
+ when Messaging::Route::SIGNATURE then Messaging::Route
94
+ when Messaging::Reset::SIGNATURE then Messaging::Reset
95
+ when Messaging::Telemetry::SIGNATURE then Messaging::Telemetry
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveCypher
4
+ module Bolt
5
+ # Handles encoding Bolt messages into Packstream format for Bolt v5.0
6
+ class MessageWriter
7
+ # Structure Markers
8
+ TINY_STRUCT_MARKER_BASE = 0xB0
9
+ STRUCT_8_MARKER = 0xDC
10
+ STRUCT_16_MARKER = 0xDD
11
+ # STRUCT_32_MARKER = 0xDE # Not implementing 32-bit sizes for now
12
+
13
+ def initialize(io)
14
+ @packer = Packstream::Packer.new(io)
15
+ @io = io # Keep a reference for direct writing if needed
16
+ end
17
+
18
+ # Encodes and writes a Bolt message to the underlying IO stream.
19
+ # @param message [Messaging::Message] The message object to write.
20
+ def write(message)
21
+ # Bolt 4.3 requires different chunking
22
+ size = message.fields.size
23
+
24
+ # Write structure header with size and signature
25
+ if size < 16
26
+ write_marker([TINY_STRUCT_MARKER_BASE | size].pack('C'))
27
+ else
28
+ write_marker([STRUCT_8_MARKER, size].pack('CC'))
29
+ end
30
+
31
+ # Write signature
32
+ write_marker([message.signature].pack('C'))
33
+
34
+ # Pack fields with careful handling of nils
35
+ message.fields.each do |field|
36
+ if field.nil?
37
+ @packer.pack(nil)
38
+ else
39
+ @packer.pack(field)
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ # Method removed as it's not used anymore - we're using the new write method above
47
+
48
+ def write_marker(marker_bytes)
49
+ @io.write(marker_bytes)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,307 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext/hash/indifferent_access'
5
+
6
+ module ActiveCypher
7
+ module Bolt
8
+ # @!parse
9
+ # # Messaging: Because every protocol needs a registry, and every registry needs a protocol.
10
+ module Messaging
11
+ # Internal registry: signature byte → Message subclass
12
+ @registry = {}
13
+
14
+ class << self
15
+ # For introspection
16
+ def registry
17
+ @registry.dup
18
+ end
19
+
20
+ # Lookup-and-instantiate based on signature + raw fields
21
+ #
22
+ # @param signature [Integer] the Bolt message signature
23
+ # @param fields [Array] the decoded fields for this message
24
+ # @return [Message] instance of the right subclass, or generic Message
25
+ def for_signature(signature, *fields)
26
+ if (klass = @registry[signature])
27
+ klass.new(*fields)
28
+ else
29
+ Message.new(signature, fields)
30
+ end
31
+ end
32
+
33
+ # Register a subclass if it defines SIGNATURE
34
+ def register(subclass)
35
+ return unless subclass.const_defined?(:SIGNATURE)
36
+
37
+ sig = subclass.const_get(:SIGNATURE)
38
+ @registry[sig] = subclass
39
+ end
40
+
41
+ # Normalize any metadata or parameters map
42
+ def normalize_map(map)
43
+ (map || {}).with_indifferent_access
44
+ end
45
+ end
46
+
47
+ # Base class — automatically registers subclasses with SIGNATURE
48
+ # Because inheritance hierarchies are the only thing deeper than this protocol.
49
+ class Message
50
+ attr_reader :signature, :fields
51
+
52
+ def initialize(signature, fields)
53
+ @signature = signature
54
+ @fields = fields
55
+ end
56
+
57
+ def ==(other)
58
+ other.class == self.class &&
59
+ other.signature == signature &&
60
+ other.fields == fields
61
+ end
62
+ alias eql? ==
63
+
64
+ def self.inherited(subclass)
65
+ Messaging.register(subclass)
66
+ super
67
+ end
68
+ end
69
+
70
+ # The HELLO message. Because every protocol needs to start with a greeting before the disappointment.
71
+ class Hello < Message
72
+ SIGNATURE = 0x01
73
+
74
+ def initialize(metadata)
75
+ meta = Messaging.normalize_map(metadata)
76
+ super(SIGNATURE, [meta])
77
+ end
78
+
79
+ def metadata
80
+ fields.first
81
+ end
82
+ end
83
+
84
+ # The GOODBYE message. For when you've had enough of this session, or life.
85
+ class Goodbye < Message
86
+ SIGNATURE = 0x02
87
+
88
+ def initialize
89
+ super(SIGNATURE, [])
90
+ end
91
+ end
92
+
93
+ # The RESET message. Because sometimes you just want to pretend nothing ever happened.
94
+ class Reset < Message
95
+ SIGNATURE = 0x0F
96
+
97
+ def initialize
98
+ super(SIGNATURE, [])
99
+ end
100
+ end
101
+
102
+ # The RUN message. Because what else would you do with a database connection?
103
+ class Run < Message
104
+ SIGNATURE = 0x10
105
+
106
+ # metadata may include bookmarks, tx_timeout, tx_metadata, mode, db
107
+ def initialize(query, parameters, metadata = {})
108
+ meta = Messaging.normalize_map(metadata)
109
+ params = Messaging.normalize_map(parameters)
110
+
111
+ # Neo4j mode normalization: single-char 'r' or 'w'
112
+ meta['mode'] = meta['mode'][0] if meta['mode'].is_a?(String) && meta['mode'].length > 1
113
+
114
+ super(SIGNATURE, [query, params, meta])
115
+ end
116
+
117
+ def query = fields[0]
118
+ def parameters = fields[1]
119
+ def metadata = fields[2]
120
+ end
121
+
122
+ # The BEGIN message. Because transactions are just promises waiting to be broken.
123
+ class Begin < Message
124
+ SIGNATURE = 0x11
125
+
126
+ # metadata may include mode, db, tx_metadata, etc.
127
+ def initialize(metadata = {})
128
+ meta = Messaging.normalize_map(metadata)
129
+
130
+ # Never set db to neo4j for memgraph
131
+ if meta['adapter'] == 'memgraph'
132
+ # For Memgraph, remove db key entirely if present
133
+ meta.delete('db')
134
+ elsif meta['mode'].is_a?(String) && meta['mode'].length == 1
135
+ # This is for Neo4j only
136
+ meta['db'] ||= 'neo4j'
137
+ end
138
+
139
+ # Set default mode if not present
140
+ meta['mode'] ||= 'write'
141
+
142
+ super(SIGNATURE, [meta])
143
+ end
144
+
145
+ def metadata
146
+ fields.first
147
+ end
148
+ end
149
+
150
+ # The COMMIT message. For when you want to pretend your changes are permanent.
151
+ class Commit < Message
152
+ SIGNATURE = 0x12
153
+
154
+ def initialize
155
+ super(SIGNATURE, [])
156
+ end
157
+ end
158
+
159
+ # The ROLLBACK message. Because sometimes you just want to undo your mistakes.
160
+ class Rollback < Message
161
+ SIGNATURE = 0x13
162
+
163
+ def initialize
164
+ super(SIGNATURE, [])
165
+ end
166
+ end
167
+
168
+ # The DISCARD message. For when you want to throw away results, or your hopes.
169
+ class Discard < Message
170
+ SIGNATURE = 0x2F
171
+
172
+ # metadata: { n: <N>, qid: <QID> }, where n = -1 means all
173
+ def initialize(metadata)
174
+ meta = Messaging.normalize_map(metadata)
175
+ super(SIGNATURE, [meta])
176
+ end
177
+
178
+ def metadata = fields.first
179
+ def n = metadata[:n] || metadata['n']
180
+ def qid = metadata[:qid] || metadata['qid']
181
+ end
182
+
183
+ # The PULL message. Because sometimes you just want to see what you got.
184
+ class Pull < Message
185
+ 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
+ end
196
+
197
+ # The ROUTE message. For when you want to pretend you have control over routing.
198
+ class Route < Message
199
+ 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
+ end
210
+
211
+ # The LOGON message. Because authentication is just another chance to be rejected.
212
+ class Logon < Message
213
+ 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
+ end
224
+
225
+ # The LOGOFF message. For when you want to leave quietly, without making a scene.
226
+ class Logoff < Message
227
+ SIGNATURE = 0x6B
228
+
229
+ def initialize
230
+ super(SIGNATURE, [])
231
+ end
232
+ end
233
+
234
+ # The TELEMETRY message. Because someone, somewhere, cares about your metrics. Probably.
235
+ class Telemetry < Message
236
+ 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
+ end
247
+
248
+ # The SUCCESS message. The rarest of all Bolt messages.
249
+ class Success < Message
250
+ 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
+ end
261
+
262
+ # The RECORD message. For when you actually get data back, against all odds.
263
+ class Record < Message
264
+ SIGNATURE = 0x71
265
+
266
+ def initialize(values)
267
+ super(SIGNATURE, [values])
268
+ end
269
+
270
+ def values
271
+ fields.first
272
+ end
273
+ end
274
+
275
+ # The IGNORED message. For when the server just can't be bothered.
276
+ class Ignored < Message
277
+ SIGNATURE = 0x7E
278
+
279
+ def initialize
280
+ super(SIGNATURE, [])
281
+ end
282
+ end
283
+
284
+ # The FAILURE message. The most honest message in the protocol.
285
+ class Failure < Message
286
+ SIGNATURE = 0x7F
287
+
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
+ def code
298
+ metadata['code']
299
+ end
300
+
301
+ def message
302
+ metadata['message']
303
+ end
304
+ end
305
+ end
306
+ end
307
+ end