surrealdb-embedded 0.7.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7b75d55cc942993d3d7ea7bb41a0cb728d744d948a7483c79fb90e1db921f2a9
4
+ data.tar.gz: 5d5a09ad19b759d6335aeab6df38e747b1ba36967a3f5314dafbb75766510c66
5
+ SHA512:
6
+ metadata.gz: 4436eead8dd1e61e2a1e3673681dc55d750b0b0d1c4a0b22bd24944ee76a0325dc815ded55b9b5de7dd6748978ebd89cba0f6e3a9180d58a6a2bfe3a6ec78b0c
7
+ data.tar.gz: 80da57f4362cd5c6db546585a7686bfca397ed96f23ef1eaecdc09673144d6b49df271fbe9e2617cba93d480adf63b5b59ea845feeeee4d33c7e96a8e3c46690
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SurrealDB
4
+ module Connections
5
+ # Embedded transport for SurrealDB via FFI to libsurrealdb_c.
6
+ #
7
+ # Supports mem://, surrealkv://, and file:// URL schemes. Uses the same
8
+ # CBOR RPC protocol as the WebSocket/HTTP transports, but calls directly
9
+ # into the C library instead of going over the network.
10
+ #
11
+ # Requires `require "surrealdb/embedded"` before use.
12
+ #
13
+ # ## Thread Safety
14
+ #
15
+ # The underlying C library handles its own threading via a Tokio runtime.
16
+ # All FFI calls use `blocking: true` to release the GVL. However, the
17
+ # Ruby-side request lifecycle is not atomic -- use one Client per thread.
18
+ class Embedded < Base
19
+ def initialize(url, **options)
20
+ super
21
+ @timeout = options.fetch(:timeout, SurrealDB.configuration.timeout)
22
+ @strict = options.fetch(:strict, false)
23
+ @rpc_ptr = nil
24
+ @live_handlers = {}
25
+ @mutex = Mutex.new
26
+ @notification_thread = nil
27
+ end
28
+
29
+ def connect
30
+ err_ptr = FFI::MemoryPointer.new(:pointer)
31
+ surreal_ptr = FFI::MemoryPointer.new(:pointer)
32
+
33
+ opts = Native::SrOption.new
34
+ opts[:strict] = @strict
35
+ opts[:query_timeout] = @timeout
36
+ opts[:transaction_timeout] = @timeout
37
+
38
+ ret = Native.sr_surreal_rpc_new(err_ptr, surreal_ptr, @url, opts)
39
+ check_error!(ret, err_ptr)
40
+
41
+ @rpc_ptr = surreal_ptr.read_pointer
42
+ @connected = true
43
+ start_notification_stream
44
+ log(:debug, "Embedded connection opened: #{@url}")
45
+ end
46
+
47
+ def close
48
+ return unless @connected
49
+
50
+ @connected = false
51
+ stop_notification_stream
52
+ Native.sr_surreal_rpc_free(@rpc_ptr) if @rpc_ptr
53
+ @rpc_ptr = nil
54
+ log(:debug, 'Embedded connection closed')
55
+ end
56
+
57
+ def send_request(method, params = [])
58
+ raise ConnectionError, 'not connected' unless @connected
59
+
60
+ _id, encoded = @rpc.encode_request(method, params)
61
+
62
+ err_ptr = FFI::MemoryPointer.new(:pointer)
63
+ res_ptr = FFI::MemoryPointer.new(:pointer)
64
+
65
+ ret = Native.sr_surreal_rpc_execute(
66
+ @rpc_ptr, err_ptr, res_ptr,
67
+ encoded, encoded.bytesize
68
+ )
69
+ check_error!(ret, err_ptr)
70
+
71
+ read_and_free_response(res_ptr, ret)
72
+ end
73
+
74
+ def supports_live_queries?
75
+ true
76
+ end
77
+
78
+ # @param live_query_id [String]
79
+ # @param handler [Proc, Queue]
80
+ def on_notification(live_query_id, handler)
81
+ @mutex.synchronize { @live_handlers[live_query_id] = handler }
82
+ end
83
+
84
+ # @param live_query_id [String]
85
+ def remove_notification_handler(live_query_id)
86
+ @mutex.synchronize { @live_handlers.delete(live_query_id) }
87
+ end
88
+
89
+ private
90
+
91
+ def read_and_free_response(res_ptr, length)
92
+ raw_ptr = res_ptr.read_pointer
93
+ begin
94
+ bytes = raw_ptr.read_bytes(length)
95
+ response = @rpc.decode_response(bytes)
96
+ Protocol::Response.extract_result(response)
97
+ ensure
98
+ Native.sr_free_byte_arr(raw_ptr, length)
99
+ end
100
+ end
101
+
102
+ def check_error!(ret, err_ptr)
103
+ return if ret >= 0
104
+
105
+ err_str_ptr = err_ptr.read_pointer
106
+ message = err_str_ptr.null? ? "unknown error (code #{ret})" : err_str_ptr.read_string
107
+ Native.sr_free_string(err_str_ptr) unless err_str_ptr.null?
108
+
109
+ if ret == Native::SR_FATAL
110
+ @connected = false
111
+ raise ConnectionError, "fatal error (connection poisoned): #{message}"
112
+ end
113
+
114
+ raise ServerError, message
115
+ end
116
+
117
+ def start_notification_stream
118
+ err_ptr = FFI::MemoryPointer.new(:pointer)
119
+ stream_ptr = FFI::MemoryPointer.new(:pointer)
120
+
121
+ ret = Native.sr_surreal_rpc_notifications(@rpc_ptr, err_ptr, stream_ptr)
122
+ return if ret.negative?
123
+
124
+ @stream_ptr = stream_ptr.read_pointer
125
+ @notification_thread = Thread.new do
126
+ Thread.current.report_on_exception = false
127
+ notification_loop
128
+ end
129
+ end
130
+
131
+ def notification_loop
132
+ while @connected
133
+ res_ptr = FFI::MemoryPointer.new(:pointer)
134
+ ret = Native.sr_rpc_stream_next(@stream_ptr, res_ptr)
135
+
136
+ break if ret == Native::SR_CLOSED || !@connected
137
+
138
+ next unless ret.positive?
139
+
140
+ raw_ptr = res_ptr.read_pointer
141
+ begin
142
+ bytes = raw_ptr.read_bytes(ret)
143
+ dispatch_notification(bytes)
144
+ ensure
145
+ Native.sr_free_byte_arr(raw_ptr, ret)
146
+ end
147
+ end
148
+ rescue StandardError => e
149
+ log(:warn, "Notification stream error: #{e.class}: #{e.message}")
150
+ ensure
151
+ Native.sr_rpc_stream_free(@stream_ptr) if @stream_ptr
152
+ @stream_ptr = nil
153
+ end
154
+
155
+ def dispatch_notification(bytes) # rubocop:disable Metrics/CyclomaticComplexity
156
+ raw = ::CBOR.decode(bytes)
157
+ resolved = CBOR::Decoder.resolve(raw)
158
+ result = resolved.is_a?(Hash) ? (resolved['result'] || resolved) : resolved
159
+ live_id = result['id'] if result.is_a?(Hash)
160
+ return unless live_id
161
+
162
+ handler = @mutex.synchronize { @live_handlers[live_id] }
163
+ return unless handler
164
+
165
+ case handler
166
+ when Queue then handler.push(result)
167
+ when Proc then handler.call(result)
168
+ end
169
+ end
170
+
171
+ def stop_notification_stream
172
+ @notification_thread&.join(2)
173
+ @notification_thread&.kill if @notification_thread&.alive?
174
+ @notification_thread = nil
175
+ end
176
+
177
+ def log(level, message)
178
+ SurrealDB.configuration.logger&.send(level, message)
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Opt-in entrypoint for the embedded SurrealDB connection.
4
+ # Loads the FFI bindings and the Embedded connection class.
5
+ #
6
+ # Usage:
7
+ # require "surrealdb"
8
+ # require "surrealdb/embedded"
9
+ #
10
+ # SurrealDB.connect("mem://") do |db|
11
+ # db.use("test", "test")
12
+ # # ...
13
+ # end
14
+ #
15
+ # Requires the `ffi` gem and either:
16
+ # - SURREALDB_LIB_PATH env var pointing to libsurrealdb_c
17
+ # - libsurrealdb_c installed in the system library path
18
+
19
+ require_relative 'native/platform'
20
+ require_relative 'native/ffi'
21
+ require_relative 'connections/embedded'
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+
5
+ module SurrealDB
6
+ module Native
7
+ extend FFI::Library
8
+
9
+ ffi_lib Platform.library_path
10
+
11
+ # sr_option_t is passed by value to sr_surreal_rpc_new.
12
+ class SrOption < FFI::Struct
13
+ layout :strict, :bool,
14
+ :query_timeout, :uint8,
15
+ :transaction_timeout, :uint8
16
+ end
17
+
18
+ # Return code constants from surrealdb.h
19
+ SR_NONE = 0 # success / no more items
20
+ SR_CLOSED = -1 # stream closed
21
+ SR_ERROR = -2 # recoverable error
22
+ SR_FATAL = -3 # connection poisoned, must not reuse
23
+
24
+ # Connection lifecycle
25
+ attach_function :sr_surreal_rpc_new,
26
+ [:pointer, :pointer, :string, SrOption.by_value], :int,
27
+ blocking: true
28
+ attach_function :sr_surreal_rpc_free,
29
+ [:pointer], :void
30
+
31
+ # CBOR request/response
32
+ attach_function :sr_surreal_rpc_execute,
33
+ %i[pointer pointer pointer pointer int], :int,
34
+ blocking: true
35
+
36
+ # Live query notification stream
37
+ attach_function :sr_surreal_rpc_notifications,
38
+ %i[pointer pointer pointer], :int,
39
+ blocking: true
40
+ attach_function :sr_rpc_stream_next,
41
+ %i[pointer pointer], :int,
42
+ blocking: true
43
+ attach_function :sr_rpc_stream_free,
44
+ [:pointer], :void
45
+
46
+ # Memory cleanup
47
+ attach_function :sr_free_string, [:pointer], :void
48
+ attach_function :sr_free_byte_arr, %i[pointer int], :void
49
+ end
50
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SurrealDB
4
+ module Native
5
+ # Resolves the platform-specific path to the libsurrealdb_c shared library.
6
+ module Platform
7
+ module_function
8
+
9
+ # @return [String] absolute path or library name for FFI.ffi_lib
10
+ # @raise [LoadError] if the library cannot be found
11
+ def library_path
12
+ from_env || from_system
13
+ end
14
+
15
+ # @return [String] shared library file extension for the current OS
16
+ def library_extension
17
+ case host_os
18
+ when :macos then 'dylib'
19
+ when :windows then 'dll'
20
+ else 'so'
21
+ end
22
+ end
23
+
24
+ # @return [String] expected library filename
25
+ def library_name
26
+ case host_os
27
+ when :windows then 'surrealdb_c.dll'
28
+ else "libsurrealdb_c.#{library_extension}"
29
+ end
30
+ end
31
+
32
+ # @return [Symbol] :linux, :macos, or :windows
33
+ def host_os
34
+ case RbConfig::CONFIG['host_os']
35
+ when /darwin/i then :macos
36
+ when /mingw|mswin|cygwin/i then :windows
37
+ else :linux
38
+ end
39
+ end
40
+
41
+ # @return [Symbol] :x86_64 or :aarch64
42
+ def host_cpu
43
+ case RbConfig::CONFIG['host_cpu']
44
+ when /x86_64|amd64/i then :x86_64
45
+ when /aarch64|arm64/i then :aarch64
46
+ else RbConfig::CONFIG['host_cpu'].to_sym
47
+ end
48
+ end
49
+
50
+ def from_env
51
+ path = ENV.fetch('SURREALDB_LIB_PATH', nil)
52
+ return nil if path.nil? || path.empty?
53
+ raise LoadError, "SURREALDB_LIB_PATH points to missing file: #{path}" unless File.exist?(path)
54
+
55
+ path
56
+ end
57
+
58
+ def from_system
59
+ library_name
60
+ end
61
+
62
+ private_class_method :from_env, :from_system
63
+ end
64
+ end
65
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: surrealdb-embedded
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.7.0
5
+ platform: ruby
6
+ authors:
7
+ - SurrealDB
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ffi
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.15'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.15'
26
+ - !ruby/object:Gem::Dependency
27
+ name: surrealdb
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - '='
31
+ - !ruby/object:Gem::Version
32
+ version: 0.7.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - '='
38
+ - !ruby/object:Gem::Version
39
+ version: 0.7.0
40
+ description: Adds embedded database support (mem://, surrealkv://, file://) to the
41
+ SurrealDB Ruby SDK via FFI bindings to libsurrealdb_c. Requires libsurrealdb_c to
42
+ be installed or SURREALDB_LIB_PATH to be set.
43
+ email:
44
+ - stu.schwartz@surrealdb.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - lib/surrealdb/connections/embedded.rb
50
+ - lib/surrealdb/embedded.rb
51
+ - lib/surrealdb/native/ffi.rb
52
+ - lib/surrealdb/native/platform.rb
53
+ homepage: https://github.com/surrealdb/surrealdb.rb
54
+ licenses:
55
+ - Apache-2.0
56
+ metadata:
57
+ homepage_uri: https://github.com/surrealdb/surrealdb.rb
58
+ source_code_uri: https://github.com/surrealdb/surrealdb.rb
59
+ rubygems_mfa_required: 'true'
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '3.2'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubygems_version: 3.7.1
75
+ specification_version: 4
76
+ summary: Embedded SurrealDB engine for the Ruby SDK
77
+ test_files: []