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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f8ea5d62b319951e5324da7495acd2b082dc3c05945b9c7cd8810f81ab204ed
4
- data.tar.gz: 773874cbee89f62cea0754249800a087f8de553e79a90c7f0d5d6e85026f0a39
3
+ metadata.gz: 0146d77d1a64f774c074a7675c2c10be3cb9c00b65e1e60fead33c7baf5d4b48
4
+ data.tar.gz: 6490a676adbdc20b430a45eebd356d155d5f882ab76aab8b94af4a6302cbe5cf
5
5
  SHA512:
6
- metadata.gz: 4c6cd6f4f39cd9aceedaaf0a373e8deedbe7a217ea9fdd0e04231bfc28409eeceb8cd84294b776ec1a48d8621fdf797e5007bc4203ff9bec950d95799333c128
7
- data.tar.gz: 302b1dcfc03a25f1ac6ef126d99c9a1cfcb0c2c04dbf44414f8e4558e20b9029c8c0c5f2153f4f0de4326ff8ccb421944970ddb25412822d6cfc01887296050b
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.
@@ -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, keyword_init: true) do
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
- LOOPBACK_RE.match?(host)
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 prepare(sql)
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
- Statement.new(self, stmt_id)
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, Protocol.encode_hello(Protocol::VERSION, 0, "\x00" * 8, cfg.database, cfg.user, cfg.realm))
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
@@ -10,8 +10,16 @@ module NextSQL
10
10
  class Error < StandardError
11
11
  attr_reader :error_code
12
12
 
13
- def initialize(error_code, message = "")
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