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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +46 -0
- data/LICENSE +201 -0
- data/README.md +377 -0
- data/lib/tricoredb/builders.rb +142 -0
- data/lib/tricoredb/client.rb +979 -0
- data/lib/tricoredb/errors.rb +99 -0
- data/lib/tricoredb/frame.rb +106 -0
- data/lib/tricoredb/params.rb +121 -0
- data/lib/tricoredb/pool.rb +159 -0
- data/lib/tricoredb/response.rb +131 -0
- data/lib/tricoredb/transport.rb +184 -0
- data/lib/tricoredb/version.rb +6 -0
- data/lib/tricoredb.rb +21 -0
- data/tricoredb.gemspec +30 -0
- metadata +92 -0
|
@@ -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
|
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: []
|