tricoredb 0.1.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.
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "openssl"
5
+
6
+ module TriCoreDB
7
+ # A framed byte stream over TCP or TLS, with deadline-bounded reads.
8
+ class Transport
9
+ # @return [IO] the underlying socket (a TCPSocket, an SSLSocket, or any IO in tests)
10
+ attr_reader :io
11
+
12
+ # Open a TCP connection, optionally wrapped in TLS.
13
+ #
14
+ # @param host [String]
15
+ # @param port [Integer]
16
+ # @param connect_timeout [Numeric, nil] seconds for TCP connect and the TLS handshake
17
+ # @param tls [Hash, true, nil] see {Transport.tls_context}
18
+ # @return [Transport]
19
+ # @raise [ConnectionError]
20
+ def self.open(host, port, connect_timeout: 10, tls: nil)
21
+ sock = begin
22
+ Socket.tcp(host, port, connect_timeout: connect_timeout)
23
+ rescue SystemCallError, SocketError, IOError => e
24
+ raise ConnectionError, "connect to #{host}:#{port} failed: #{e.message}"
25
+ rescue StandardError => e
26
+ raise ConnectionError, "connect to #{host}:#{port} failed: #{e.class}: #{e.message}" if e.class.name.include?("Timeout")
27
+
28
+ raise
29
+ end
30
+ sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
31
+ return new(sock) if tls.nil? || tls == false
32
+
33
+ opts = tls == true ? {} : tls
34
+ new(start_tls(sock, host, opts, connect_timeout))
35
+ end
36
+
37
+ # Build the SSL context.
38
+ #
39
+ # Accepted options:
40
+ # - `ca_file`: PEM bundle used to verify the server; without it the system trust store is used
41
+ # - `server_name`: expected certificate name and SNI (default: the host)
42
+ # - `client_cert_file` / `client_key_file`: identity for mutual TLS, both or neither
43
+ # - `danger_accept_invalid_certs`: skip verification entirely (development only)
44
+ #
45
+ # @param opts [Hash]
46
+ # @return [OpenSSL::SSL::SSLContext]
47
+ def self.tls_context(opts)
48
+ opts = opts.transform_keys(&:to_sym)
49
+ cert_file = opts[:client_cert_file]
50
+ key_file = opts[:client_key_file]
51
+ if cert_file.nil? != key_file.nil?
52
+ missing = cert_file.nil? ? "client_cert_file" : "client_key_file"
53
+ raise ArgumentError, "tls #{missing} is required alongside the other (both are needed for mutual TLS)"
54
+ end
55
+
56
+ ctx = OpenSSL::SSL::SSLContext.new
57
+ ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION
58
+ if opts[:danger_accept_invalid_certs]
59
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
60
+ else
61
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
62
+ store = OpenSSL::X509::Store.new
63
+ if opts[:ca_file]
64
+ read_file(opts[:ca_file], "ca_file")
65
+ store.add_file(opts[:ca_file])
66
+ else
67
+ store.set_default_paths
68
+ end
69
+ ctx.cert_store = store
70
+ end
71
+ if cert_file
72
+ ctx.cert = OpenSSL::X509::Certificate.new(read_file(cert_file, "client_cert_file"))
73
+ ctx.key = OpenSSL::PKey.read(read_file(key_file, "client_key_file"))
74
+ end
75
+ ctx
76
+ end
77
+
78
+ def self.read_file(path, label)
79
+ File.binread(path)
80
+ rescue SystemCallError => e
81
+ raise ArgumentError, "tls #{label} `#{path}`: #{e.class.name.split('::').last}"
82
+ end
83
+ private_class_method :read_file
84
+
85
+ def self.start_tls(sock, host, opts, timeout)
86
+ opts = opts.transform_keys(&:to_sym)
87
+ ctx = tls_context(opts)
88
+ server_name = opts[:server_name] || host
89
+ ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
90
+ ssl.hostname = server_name if server_name && server_name !~ /\A[\d.:]+\z/
91
+ ssl.sync_close = true
92
+ ssl.sync = true
93
+ deadline = timeout && monotonic + timeout
94
+ loop do
95
+ result = ssl.connect_nonblock(exception: false)
96
+ break unless result.is_a?(Symbol)
97
+
98
+ wait(ssl, result, deadline) || raise(ConnectionError, "tls handshake with #{host} timed out")
99
+ end
100
+ ssl.post_connection_check(server_name) unless opts[:danger_accept_invalid_certs]
101
+ ssl
102
+ rescue OpenSSL::SSL::SSLError, SystemCallError, IOError => e
103
+ sock.close unless sock.closed?
104
+ raise ConnectionError, "tls verification of `#{server_name}` failed: #{e.message}"
105
+ end
106
+ private_class_method :start_tls
107
+
108
+ def self.monotonic
109
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
110
+ end
111
+
112
+ # @return [Boolean] false on timeout
113
+ def self.wait(io, what, deadline)
114
+ remaining = deadline && (deadline - monotonic)
115
+ return false if remaining && remaining <= 0
116
+
117
+ ready = what == :wait_writable ? IO.select(nil, [io], nil, remaining) : IO.select([io], nil, nil, remaining)
118
+ !ready.nil?
119
+ end
120
+
121
+ # @param io [IO]
122
+ def initialize(io)
123
+ @io = io
124
+ end
125
+
126
+ # @return [Boolean]
127
+ def closed?
128
+ @io.nil?
129
+ end
130
+
131
+ # Write one frame.
132
+ def write_frame(tag, payload)
133
+ bytes = Frame.encode(tag, payload)
134
+ raise ConnectionError, "connection is closed" if @io.nil?
135
+
136
+ @io.write(bytes)
137
+ @io.flush if @io.respond_to?(:flush)
138
+ nil
139
+ end
140
+
141
+ # Read one frame.
142
+ #
143
+ # @param timeout [Numeric, nil] seconds; nil waits indefinitely
144
+ # @return [Array(Integer, Object)] `[tag, decoded_payload]`
145
+ def read_frame(timeout)
146
+ deadline = timeout && self.class.monotonic + timeout
147
+ tag, length = Frame.decode_header(read_exactly(Frame::HEADER_SIZE, deadline, timeout))
148
+ body = length.zero? ? "".b : read_exactly(length, deadline, timeout)
149
+ [tag, Frame.decode_body(body)]
150
+ end
151
+
152
+ # Close the socket. Idempotent.
153
+ def close
154
+ io = @io
155
+ @io = nil
156
+ io&.close
157
+ rescue StandardError
158
+ nil
159
+ end
160
+
161
+ private
162
+
163
+ def read_exactly(n, deadline, timeout)
164
+ buf = +"".b
165
+ while buf.bytesize < n
166
+ raise ConnectionError, "connection is closed" if @io.nil?
167
+
168
+ chunk = @io.read_nonblock(n - buf.bytesize, exception: false)
169
+ case chunk
170
+ when :wait_readable, :wait_writable
171
+ unless self.class.wait(@io, chunk, deadline)
172
+ raise ReadTimeout, "no reply within #{timeout}s; the connection is closed because the reply may still " \
173
+ "be in flight and would be read as the next request's answer"
174
+ end
175
+ when nil
176
+ raise ConnectionError, "connection closed by the server"
177
+ else
178
+ buf << chunk
179
+ end
180
+ end
181
+ buf
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TriCoreDB
4
+ # The version of this gem. Distinct from {Frame::VERSION}, the wire format.
5
+ VERSION = "0.1.0"
6
+ end
data/lib/tricoredb.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tricoredb/version"
4
+ require_relative "tricoredb/errors"
5
+ require_relative "tricoredb/frame"
6
+ require_relative "tricoredb/params"
7
+ require_relative "tricoredb/builders"
8
+ require_relative "tricoredb/response"
9
+ require_relative "tricoredb/transport"
10
+ require_relative "tricoredb/client"
11
+ require_relative "tricoredb/pool"
12
+
13
+ # Ruby driver for TriCoreDB's native `tricore` protocol.
14
+ module TriCoreDB
15
+ # Shorthand for {Client.connect}.
16
+ #
17
+ # @return [Client]
18
+ def self.connect(**options)
19
+ Client.connect(**options)
20
+ end
21
+ end
data/tricoredb.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/tricoredb/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "tricoredb"
7
+ spec.version = TriCoreDB::VERSION
8
+ spec.authors = ["Trinesh Kumar"]
9
+ spec.summary = "Ruby driver for TriCoreDB's native tricore protocol"
10
+ spec.description = "A dependency-free Ruby client for TriCoreDB: SQL with server-side parameters, " \
11
+ "session transactions, document, vector, graph, cache, LLM context and admin operations " \
12
+ "over the native framed protocol, with TLS, timeouts, cancellation and a thread-safe pool."
13
+ spec.license = "Apache-2.0"
14
+ spec.homepage = "https://github.com/trinesh14/tricoredb-sdk-ruby"
15
+ spec.required_ruby_version = ">= 3.1"
16
+
17
+ spec.metadata = {
18
+ "source_code_uri" => spec.homepage,
19
+ "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md",
20
+ "bug_tracker_uri" => "#{spec.homepage}/issues",
21
+ "documentation_uri" => "https://rubydoc.info/gems/tricoredb",
22
+ "rubygems_mfa_required" => "true"
23
+ }
24
+
25
+ spec.files = Dir.glob("lib/**/*.rb") + %w[README.md LICENSE CHANGELOG.md tricoredb.gemspec]
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_development_dependency "minitest", "~> 5.16"
29
+ spec.add_development_dependency "rake", "~> 13.0"
30
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tricoredb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Trinesh Kumar
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-09-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ description: 'A dependency-free Ruby client for TriCoreDB: SQL with server-side parameters,
42
+ session transactions, document, vector, graph, cache, LLM context and admin operations
43
+ over the native framed protocol, with TLS, timeouts, cancellation and a thread-safe
44
+ pool.'
45
+ email:
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - CHANGELOG.md
51
+ - LICENSE
52
+ - README.md
53
+ - lib/tricoredb.rb
54
+ - lib/tricoredb/builders.rb
55
+ - lib/tricoredb/client.rb
56
+ - lib/tricoredb/errors.rb
57
+ - lib/tricoredb/frame.rb
58
+ - lib/tricoredb/params.rb
59
+ - lib/tricoredb/pool.rb
60
+ - lib/tricoredb/response.rb
61
+ - lib/tricoredb/transport.rb
62
+ - lib/tricoredb/version.rb
63
+ - tricoredb.gemspec
64
+ homepage: https://github.com/trinesh14/tricoredb-sdk-ruby
65
+ licenses:
66
+ - Apache-2.0
67
+ metadata:
68
+ source_code_uri: https://github.com/trinesh14/tricoredb-sdk-ruby
69
+ changelog_uri: https://github.com/trinesh14/tricoredb-sdk-ruby/blob/main/CHANGELOG.md
70
+ bug_tracker_uri: https://github.com/trinesh14/tricoredb-sdk-ruby/issues
71
+ documentation_uri: https://rubydoc.info/gems/tricoredb
72
+ rubygems_mfa_required: 'true'
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '3.1'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubygems_version: 3.5.22
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: Ruby driver for TriCoreDB's native tricore protocol
92
+ test_files: []