bzync-nextsql 0.1.0 → 0.2.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 +4 -4
- data/README.md +16 -0
- data/lib/nextsql/client.rb +85 -9
- data/lib/nextsql/errors.rb +9 -1
- data/lib/nextsql/field_encryption.rb +823 -0
- data/lib/nextsql/pool.rb +215 -0
- data/lib/nextsql/protocol.rb +26 -5
- data/lib/nextsql/version.rb +1 -1
- data/lib/nextsql.rb +7 -0
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0146d77d1a64f774c074a7675c2c10be3cb9c00b65e1e60fead33c7baf5d4b48
|
|
4
|
+
data.tar.gz: 6490a676adbdc20b430a45eebd356d155d5f882ab76aab8b94af4a6302cbe5cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6d7e2921ed7f87454c6d65893d06178a9f776dc451b04e0258005f8a7d210f870f4fc9fc94e6cf9ad69a650b43a58b0bc22814844772ab96c7e968b31181bf06
|
|
7
|
+
data.tar.gz: 7cc3a24342dbaacb69cb828c6fc7214b618dce91eb6ca4c832019d681e5f7915da4c5f8456039805189fe4eaeb80f9ce5cbec231a912014ff5d18cb78b9d6042
|
data/README.md
CHANGED
|
@@ -31,7 +31,23 @@ end
|
|
|
31
31
|
Plaintext connections are allowed only on loopback. For an HA cluster with
|
|
32
32
|
follower-read routing, use `NextSQL.connect_cluster`.
|
|
33
33
|
|
|
34
|
+
### Connection Pooling & Statement Caching
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
pool = NextSQL.create_pool(cfg, min_size: 2, max_size: 10, statement_cache_size: 50)
|
|
38
|
+
begin
|
|
39
|
+
pool.with do |conn|
|
|
40
|
+
res = conn.exec("SELECT * FROM users WHERE status = $1", ["active"])
|
|
41
|
+
end
|
|
42
|
+
ensure
|
|
43
|
+
pool.close
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Parameterized queries automatically reuse cached server-side prepared statements via LRU eviction policy.
|
|
48
|
+
|
|
34
49
|
- Full driver docs: <https://nextsql.bzync.com/docs/drivers>
|
|
35
50
|
- Wire protocol: <https://github.com/bzync/nextsql/blob/master/docs/protocol.md>
|
|
51
|
+
- Client-side field encryption: <https://nextsql.bzync.com/docs/client-encryption>
|
|
36
52
|
|
|
37
53
|
MIT licensed.
|
data/lib/nextsql/client.rb
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
require "socket"
|
|
8
8
|
require "openssl"
|
|
9
|
+
require "ipaddr"
|
|
9
10
|
|
|
10
11
|
require_relative "protocol"
|
|
11
12
|
require_relative "errors"
|
|
13
|
+
require_relative "field_encryption"
|
|
12
14
|
|
|
13
15
|
module NextSQL
|
|
14
16
|
# TLS options for a remote connection. +ca+ is PEM text/bytes; omit both
|
|
@@ -23,12 +25,14 @@ module NextSQL
|
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
Config = Struct.new(:address, :nodes, :database, :realm, :user, :password, :key, :key_version, :tls,
|
|
26
|
-
:insecure_no_tls, :read_consistency, :max_staleness_ms, :timeout,
|
|
28
|
+
:insecure_no_tls, :read_consistency, :max_staleness_ms, :timeout, :field_keys,
|
|
29
|
+
:statement_cache_size, keyword_init: true) do
|
|
27
30
|
def initialize(**kwargs)
|
|
28
31
|
defaults = {
|
|
29
32
|
address: "", nodes: [], database: "", realm: "", user: "", password: "",
|
|
30
33
|
key: nil, key_version: 1, tls: nil, insecure_no_tls: false,
|
|
31
|
-
read_consistency: Protocol::READ_STRONG, max_staleness_ms: 0, timeout: 60.0
|
|
34
|
+
read_consistency: Protocol::READ_STRONG, max_staleness_ms: 0, timeout: 60.0,
|
|
35
|
+
field_keys: nil, statement_cache_size: 50
|
|
32
36
|
}
|
|
33
37
|
super(defaults.merge(kwargs))
|
|
34
38
|
end
|
|
@@ -40,7 +44,6 @@ module NextSQL
|
|
|
40
44
|
# from multiple threads/fibers — open one Connection per worker, or use
|
|
41
45
|
# +Cluster+ which pools one connection per node.
|
|
42
46
|
class Connection
|
|
43
|
-
LOOPBACK_RE = /\A127\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/.freeze
|
|
44
47
|
CONNECT_TIMEOUT = 10.0
|
|
45
48
|
|
|
46
49
|
class << self
|
|
@@ -73,9 +76,12 @@ module NextSQL
|
|
|
73
76
|
host, = split_host_port(addr, allow_bare: true)
|
|
74
77
|
host = host.strip.downcase
|
|
75
78
|
return true if host == "localhost"
|
|
76
|
-
return true if %w[::1 0:0:0:0:0:0:0:1].include?(host)
|
|
77
79
|
|
|
78
|
-
|
|
80
|
+
begin
|
|
81
|
+
IPAddr.new(host).loopback?
|
|
82
|
+
rescue IPAddr::Error
|
|
83
|
+
false
|
|
84
|
+
end
|
|
79
85
|
end
|
|
80
86
|
|
|
81
87
|
def validate_config!(cfg)
|
|
@@ -92,6 +98,7 @@ module NextSQL
|
|
|
92
98
|
raise Error.new("invalid_argument", "plaintext is only allowed on loopback")
|
|
93
99
|
end
|
|
94
100
|
raise Error.new("invalid_argument", "user is required") if cfg.user.to_s.empty?
|
|
101
|
+
raise Error.new("invalid_argument", "statement_cache_size must be >= 0") if cfg.statement_cache_size && cfg.statement_cache_size < 0
|
|
95
102
|
end
|
|
96
103
|
|
|
97
104
|
LEADING_WS_RE = /\A[ \t\r\n(]+/.freeze
|
|
@@ -172,6 +179,8 @@ module NextSQL
|
|
|
172
179
|
@sock = self.class.dial(cfg)
|
|
173
180
|
@secret = "".b
|
|
174
181
|
@busy = false
|
|
182
|
+
@statement_cache_size = cfg.statement_cache_size || 0
|
|
183
|
+
@stmt_cache = {}
|
|
175
184
|
begin
|
|
176
185
|
handshake
|
|
177
186
|
set_read_consistency(cfg.read_consistency, cfg.max_staleness_ms) if cfg.read_consistency != Protocol::READ_STRONG
|
|
@@ -200,6 +209,30 @@ module NextSQL
|
|
|
200
209
|
st
|
|
201
210
|
end
|
|
202
211
|
|
|
212
|
+
def encrypt_field(table, column, field_type, value)
|
|
213
|
+
raise Error.new("invalid_argument", "field_keys is required for client encryption") unless @cfg.field_keys
|
|
214
|
+
|
|
215
|
+
FieldEncryption.encrypt(@cfg.field_keys, @cfg.database.to_s.empty? ? "default" : @cfg.database, table, column, field_type, value)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def decrypt_field(table, column, expected_type, ciphertext)
|
|
219
|
+
raise Error.new("invalid_argument", "field_keys is required for client encryption") unless @cfg.field_keys
|
|
220
|
+
|
|
221
|
+
FieldEncryption.decrypt(@cfg.field_keys, @cfg.database.to_s.empty? ? "default" : @cfg.database, table, column, expected_type, ciphertext)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def encrypt_field_deterministic(table, column, field_type, value)
|
|
225
|
+
raise Error.new("invalid_argument", "field_keys is required for client encryption") unless @cfg.field_keys
|
|
226
|
+
|
|
227
|
+
FieldEncryption.encrypt_deterministic(@cfg.field_keys, @cfg.database.to_s.empty? ? "default" : @cfg.database, table, column, field_type, value)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def decrypt_field_deterministic(table, column, expected_type, ciphertext)
|
|
231
|
+
raise Error.new("invalid_argument", "field_keys is required for client encryption") unless @cfg.field_keys
|
|
232
|
+
|
|
233
|
+
FieldEncryption.decrypt_deterministic(@cfg.field_keys, @cfg.database.to_s.empty? ? "default" : @cfg.database, table, column, expected_type, ciphertext)
|
|
234
|
+
end
|
|
235
|
+
|
|
203
236
|
def exec(sql, params = [])
|
|
204
237
|
query(sql, params).collect
|
|
205
238
|
end
|
|
@@ -208,6 +241,29 @@ module NextSQL
|
|
|
208
241
|
raise Error.new("unavailable", "connection closed") if @sock.nil?
|
|
209
242
|
raise Error.new("conflict", "connection is busy") if @busy
|
|
210
243
|
|
|
244
|
+
if !params.empty? && @statement_cache_size > 0
|
|
245
|
+
stmt_id = @stmt_cache.delete(sql)
|
|
246
|
+
if stmt_id.nil?
|
|
247
|
+
stmt_id = prepare_id(sql)
|
|
248
|
+
if @stmt_cache.size >= @statement_cache_size
|
|
249
|
+
_evicted_sql, evicted_id = @stmt_cache.shift
|
|
250
|
+
begin
|
|
251
|
+
close_statement(evicted_id)
|
|
252
|
+
rescue StandardError
|
|
253
|
+
nil
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
@stmt_cache[sql] = stmt_id
|
|
258
|
+
|
|
259
|
+
begin
|
|
260
|
+
return execute_prepared(stmt_id, params)
|
|
261
|
+
rescue StandardError
|
|
262
|
+
@stmt_cache.delete(sql)
|
|
263
|
+
raise
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
211
267
|
@busy = true
|
|
212
268
|
begin
|
|
213
269
|
write_frame(Protocol::TYPE_QUERY, Protocol.encode_query(sql, params))
|
|
@@ -238,7 +294,7 @@ module NextSQL
|
|
|
238
294
|
end
|
|
239
295
|
end
|
|
240
296
|
|
|
241
|
-
def
|
|
297
|
+
def prepare_id(sql)
|
|
242
298
|
raise Error.new("conflict", "connection is busy") if @busy
|
|
243
299
|
|
|
244
300
|
write_frame(Protocol::TYPE_PREPARE, Protocol.u32bytes(sql.b, Protocol::MAX_SQL))
|
|
@@ -248,7 +304,11 @@ module NextSQL
|
|
|
248
304
|
|
|
249
305
|
stmt_id = Protocol.u32(payload, 0)
|
|
250
306
|
expect_ready
|
|
251
|
-
|
|
307
|
+
stmt_id
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def prepare(sql)
|
|
311
|
+
Statement.new(self, prepare_id(sql))
|
|
252
312
|
end
|
|
253
313
|
|
|
254
314
|
def execute_prepared(stmt_id, params)
|
|
@@ -297,6 +357,17 @@ module NextSQL
|
|
|
297
357
|
def close
|
|
298
358
|
return if @sock.nil?
|
|
299
359
|
|
|
360
|
+
if @stmt_cache
|
|
361
|
+
@stmt_cache.each_value do |stmt_id|
|
|
362
|
+
begin
|
|
363
|
+
close_statement(stmt_id)
|
|
364
|
+
rescue StandardError
|
|
365
|
+
nil
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
@stmt_cache.clear
|
|
369
|
+
end
|
|
370
|
+
|
|
300
371
|
begin
|
|
301
372
|
write_frame(Protocol::TYPE_TERMINATE, "")
|
|
302
373
|
rescue Error
|
|
@@ -369,12 +440,17 @@ module NextSQL
|
|
|
369
440
|
|
|
370
441
|
def handshake
|
|
371
442
|
cfg = @cfg
|
|
372
|
-
write_frame(Protocol::TYPE_HELLO,
|
|
443
|
+
write_frame(Protocol::TYPE_HELLO,
|
|
444
|
+
Protocol.encode_hello(Protocol::VERSION, Protocol::FLAG_PUBLIC_ERROR_CODES, "\x00" * 8,
|
|
445
|
+
cfg.database, cfg.user, cfg.realm))
|
|
373
446
|
typ, payload = read_frame
|
|
374
447
|
raise unexpected(typ, payload) if typ != Protocol::TYPE_HELLO_OK
|
|
375
448
|
|
|
376
|
-
_version, auth_method, secret = Protocol.decode_hello_ok(payload)
|
|
449
|
+
_version, auth_method, secret, flags = Protocol.decode_hello_ok(payload)
|
|
377
450
|
@secret = secret
|
|
451
|
+
# Diagnostic only: decode_error reads the field whenever it is present,
|
|
452
|
+
# so nothing depends on this having been echoed.
|
|
453
|
+
@public_error_codes = (flags & Protocol::FLAG_PUBLIC_ERROR_CODES) != 0
|
|
378
454
|
write_frame(Protocol::TYPE_AUTH, Protocol.u16str(cfg.password))
|
|
379
455
|
typ, payload = read_frame
|
|
380
456
|
raise unexpected(typ, payload) if typ != Protocol::TYPE_AUTH_OK
|
data/lib/nextsql/errors.rb
CHANGED
|
@@ -10,8 +10,16 @@ module NextSQL
|
|
|
10
10
|
class Error < StandardError
|
|
11
11
|
attr_reader :error_code
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
# +public_code+ is the stable ERR_* name from docs/error-codes.md. It is
|
|
14
|
+
# set only on an error received over a connection that negotiated the
|
|
15
|
+
# public taxonomy, and is "" otherwise -- for a local error, or an older
|
|
16
|
+
# server. +error_code+ always carries the legacy class, so retry logic
|
|
17
|
+
# keeps reading that and nothing should require +public_code+.
|
|
18
|
+
attr_reader :public_code
|
|
19
|
+
|
|
20
|
+
def initialize(error_code, message = "", public_code = "")
|
|
14
21
|
@error_code = error_code
|
|
22
|
+
@public_code = public_code
|
|
15
23
|
super(message.empty? ? error_code : message)
|
|
16
24
|
end
|
|
17
25
|
end
|