skaidb 1.2.0 → 1.4.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: 7a9f39f07d8b6d8abb6edf43d48b4c70da568b4a66f62044fb6f36d7b7b0e1ae
4
- data.tar.gz: bb205511e213fe77efcfbaff5c17b82c453fcc2135cee1c362e154b9ad169e6f
3
+ metadata.gz: f947092b55d66cc8d7c55153cbea09ebcdd0298f48fcb6c97ea0f3a5edd5ac01
4
+ data.tar.gz: 9b472d16803f161f2aa6d4554c19aede65f9a227b79d29c7f015204c04a7bcc7
5
5
  SHA512:
6
- metadata.gz: 9ce5348688c5bd6cd480006b2fa567ed7af74cc0ffe557f387d55705effd49dcbd754b8c52b32a30ae3361234226e4a82828bff7dc0d3d43f9a03e086849c53b
7
- data.tar.gz: e7d6fd76c93392bbea0e2583b5741b418188c4d045d0226d7e8d0dcf2828fbe4cd03e34a32da58598d0173d81875cf8b1fe5b405c64e0ac658c108be67adf1da
6
+ metadata.gz: 68d82f58f409113c18f1536023042343ffbfa86c8ec6029a0f799cd8f88fd038f418ced9c1be77beca7826c7d5756e523de221c6bf798e7069db1e14be7aaf12
7
+ data.tar.gz: f95915e0918b17e31d2b9e63cfbcbe92ee717f07b49075bbd1da055cb49f60f592c346eb579b6c65a8dc855ddf669c41d467086dec90ccb59abc0861c784dacc
data/CHANGELOG.md CHANGED
@@ -4,6 +4,38 @@ All notable changes to the skaidb Ruby driver. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [1.4.0] - 2026-10-29
8
+
9
+ ### Fixed
10
+ - A streamed `CALL` whose procedure body `EMIT`s result sets returns them.
11
+ The server answers such a call with every set in one `ResultSets` frame
12
+ (tag 8) even to a stream request, and `conn.stream`, with or without
13
+ parameters, raised `QueryError` (`unexpected response tag 8 to stream
14
+ request`) and lost the sets. The frame is now decoded by the code `exec`
15
+ uses: the block receives the rows `exec`'s `Result` would yield (the
16
+ call's final result, the LAST set) and `stream` returns that same `:rows`
17
+ `Result`, every set in order under `result_sets`. Nothing is left in
18
+ flight, so the connection stays usable — the block may even run
19
+ statements on it.
20
+
21
+ ## [1.3.0] - 2026-10-29
22
+
23
+ ### Added
24
+ - **Parameterised streaming**: `conn.stream(sql, params)` binds `$1`, `$2`,
25
+ … exactly as `exec_params(sql, params)` does — typed, with the same values
26
+ (Arrays, Hashes, `Time`, `BigDecimal`, `Skaidb::Uuid`, …) and the same
27
+ placeholder checks — over `OP_QUERY_STREAM_PARAMS` (server ≥ 0.330), so a
28
+ range-bounded export no longer needs its values written into the SQL. It
29
+ takes `consistency:`, a block or the Enumerator form like any stream.
30
+ Without parameters (`nil` or `[]`) the request is the original
31
+ `OP_QUERY_STREAM`, byte for byte, so it works against every server; a
32
+ parameterised stream against an older server raises `QueryError`
33
+ (`server does not support parameterised streaming (skaidb >= 0.330): …`)
34
+ and leaves the connection usable. `params` that are not an Array raise
35
+ `ArgumentError`.
36
+ - The vendored conformance suite's `stream_params` case (a streamed
37
+ statement with bound parameters) runs through `stream(sql, params)`.
38
+
7
39
  ## [1.2.0] - 2026-10-25
8
40
 
9
41
  ### Added
@@ -174,6 +206,7 @@ batches, a 2 500-row stream, error handling, and the `drivers` table showing
174
206
  because it is a bundled gem since Ruby 3.4.
175
207
  - The example moved to `examples/basic.rb`.
176
208
 
209
+ [1.3.0]: https://github.com/porcupin26/skaidb-ruby/compare/v1.2.0...v1.3.0
177
210
  [1.2.0]: https://github.com/porcupin26/skaidb-ruby/compare/v1.1.0...v1.2.0
178
211
  [1.1.0]: https://github.com/porcupin26/skaidb-ruby/compare/v1.0.3...v1.1.0
179
212
  [1.0.3]: https://github.com/porcupin26/skaidb-ruby/compare/v1.0.2...v1.0.3
data/README.md CHANGED
@@ -55,13 +55,13 @@ Xcode command-line tools on macOS). `gem install skaidb` and vendoring reuse
55
55
  the `bigdecimal` already installed.
56
56
 
57
57
  Or vendor the single file: copy
58
- [`lib/skaidb.rb`](https://github.com/porcupin26/skaidb-ruby/blob/v1.2.0/lib/skaidb.rb)
58
+ [`lib/skaidb.rb`](https://github.com/porcupin26/skaidb-ruby/blob/v1.4.0/lib/skaidb.rb)
59
59
  into your project and `require_relative` it. It has no dependencies beyond
60
60
  Ruby's standard library.
61
61
 
62
62
  Every release is also attached as a `.gem` to its
63
63
  [GitHub release](https://github.com/porcupin26/skaidb-ruby/releases) for
64
- installs without registry access: `gem install ./skaidb-1.2.0.gem`.
64
+ installs without registry access: `gem install ./skaidb-1.4.0.gem`.
65
65
 
66
66
  ## Quick start
67
67
 
@@ -280,8 +280,9 @@ A result holds all its rows in memory; for results that do not fit, stream.
280
280
 
281
281
  ## Streaming large results
282
282
 
283
- `conn.stream(sql, consistency: nil)` runs the statement over the streaming
284
- opcode and yields one row Hash at a time, holding **one chunk** in memory:
283
+ `conn.stream(sql, params = [], consistency: nil)` runs the statement over the
284
+ streaming opcode and yields one row Hash at a time, holding **one chunk** in
285
+ memory:
285
286
 
286
287
  ```ruby
287
288
  conn.stream("SELECT id, pad FROM big ORDER BY id") do |row|
@@ -289,12 +290,26 @@ conn.stream("SELECT id, pad FROM big ORDER BY id") do |row|
289
290
  end
290
291
 
291
292
  first = conn.stream("SELECT id FROM big ORDER BY id").first(10) # Enumerator form
293
+
294
+ conn.stream("SELECT id, ts, body FROM events WHERE ts >= $1 AND ts < $2", [from, to]) do |row|
295
+ export(row) # bound parameters, typed
296
+ end
292
297
  ```
293
298
 
294
- `stream` takes SQL text only (no parameters). With a block it returns a
295
- `Result` whose `kind` is `:stream` and whose `fields` are the column names
296
- (the rows went to the block); a non-row statement returns its `:mutation` /
297
- `:ddl` Result without yielding.
299
+ `params` bind `$1`, `$2`, … exactly as `exec_params` binds them — typed, with
300
+ the same values accepted — and need a server ≥ 0.330: an older one raises
301
+ `QueryError` (`server does not support parameterised streaming (skaidb >=
302
+ 0.330): …`), and there is no client-side text fallback. A stream without
303
+ parameters sends the original streaming opcode, which every server
304
+ understands. The server pages a plain `SELECT` (bare columns from one table,
305
+ unordered or in storage-key order), and no scan budget applies to a streamed
306
+ page (server ≥ 0.330). With a block `stream` returns a `Result` whose `kind`
307
+ is `:stream` and whose `fields` are the column names (the rows went to the
308
+ block); a non-row statement returns its `:mutation` / `:ddl` Result without
309
+ yielding. A `CALL` whose body `EMIT`s result sets is answered with all of
310
+ them in one frame even when streamed: the block receives the rows `exec`
311
+ would return (the last set), and `stream` returns that same `Result`, every
312
+ set under `result_sets`.
298
313
 
299
314
  **The abandon/drain rule.** The connection is busy for the whole stream:
300
315
  a statement from another thread waits for it. Leaving the block early —
@@ -418,8 +433,8 @@ StandardError
418
433
  - A transport failure (`ConnectionError`) marks the connection broken; the
419
434
  next statement re-dials, or a pool replaces it.
420
435
  - `ArgumentError` for `Pool.new(maxsize: 0)` or an unknown
421
- `load_balancing`/`idle_order`, and for `Skaidb::Uuid.new` of a string that
422
- is not a UUID.
436
+ `load_balancing`/`idle_order`, for `Skaidb::Uuid.new` of a string that
437
+ is not a UUID, and for `stream` parameters that are not an Array.
423
438
 
424
439
  ## Transactions
425
440
 
@@ -446,7 +461,7 @@ frame after every handshake. It shows up as `client_name = 'ruby'` /
446
461
 
447
462
  ```sql
448
463
  SELECT client_name, client_version FROM drivers;
449
- -- ruby | 1.2.0
464
+ -- ruby | 1.4.0
450
465
  ```
451
466
 
452
467
  The server records the row asynchronously, so a `SELECT` immediately after
@@ -461,7 +476,8 @@ know the opcode answers with an error the driver ignores.
461
476
  falls back: prepared statements → client-side text binding, `Hello` →
462
477
  ignored, pool discovery (`SHOW ENDPOINTS`, server ≥ 0.327) → the
463
478
  configured seeds. `stream` and `exec_batch` need a server with those
464
- opcodes and raise `QueryError` otherwise.
479
+ opcodes, and `stream` with parameters a server ≥ 0.330; they raise
480
+ `QueryError` otherwise.
465
481
  - The wire protocol (framing, SCRAM handshake, value encoding, opcodes) is
466
482
  specified at <https://skaidb.org/docs/PROTOCOL.html>. The driver speaks the
467
483
  binary protocol on port 7000; the server's REST/JSON gateway (port 7080)
data/docs/api.md CHANGED
@@ -55,11 +55,22 @@ Raises `Skaidb::Error` before dialing for an unknown `auth_mechanism`, a
55
55
  - `prepare_server(sql) → [id, nparams] | nil` — prepare a statement with `?`
56
56
  placeholders on the server; `nil` when the server refuses. Cached per
57
57
  connection (240 entries); the cache is cleared on re-dial.
58
- - `stream(sql, consistency: nil) { |row| … } → Result` — see
59
- [streaming.md](streaming.md). For a row stream the returned Result has
58
+ - `stream(sql, params = [], consistency: nil) { |row| … } → Result` — see
59
+ [streaming.md](streaming.md). `params` (an Array) bind `$1`, `$2`, …
60
+ exactly as `exec_params` binds them, typed, over `OP_QUERY_STREAM_PARAMS`
61
+ (server ≥ 0.330; an older one raises `QueryError` `server does not support
62
+ parameterised streaming (skaidb >= 0.330): …`, with no text fallback);
63
+ without parameters (`nil` or `[]`) the request is the original
64
+ `OP_QUERY_STREAM`, which every server understands. A placeholder/parameter
65
+ mismatch or a value that cannot be bound raises `QueryError` before
66
+ anything is sent, and `params` that are not an Array raise
67
+ `ArgumentError`. For a row stream the returned Result has
60
68
  `kind == :stream`, the column names in `fields` and no `rows` (they went to
61
69
  the block); a statement without rows returns its `:mutation` / `:ddl`
62
- Result without yielding. Without a block returns an Enumerator.
70
+ Result without yielding; a `CALL` whose body `EMIT`s result sets (answered
71
+ in one frame even when streamed) yields the rows `exec` would return (the
72
+ last set) and returns that same `:rows` Result, every set under
73
+ `result_sets`. Without a block returns an Enumerator.
63
74
  - `subscribe(stream_name, after: nil, poll: 0.5) { |event| … }` — yields a
64
75
  `CREATE STREAM` log's events forever, each a Hash with `"id"`, `"op"`,
65
76
  `"k"`, `"ts"`, `"doc"`. Polls `_stream_<name>` 500 rows at a time with a
data/docs/pooling.md CHANGED
@@ -122,6 +122,12 @@ latest writer — pass a key when you balance. A statement is a write unless
122
122
  its first keyword (after whitespace, comments and opening parentheses) is
123
123
  `SELECT`, `SHOW`, `EXPLAIN`, `DESCRIBE`, `DESC`, `VALUES`, `WITH` or `USE`.
124
124
 
125
+ Derive the key without touching the pool: if it comes from something that
126
+ itself queries the database through the same pool (a web framework's
127
+ "current user" loader), a helper that checks out a connection and computes
128
+ the key inside it re-enters itself. Read the raw id from the session or
129
+ token instead.
130
+
125
131
  **Watching it.** `pool.members` returns one Hash per member, sorted by
126
132
  endpoint:
127
133
 
data/docs/streaming.md CHANGED
@@ -4,7 +4,12 @@
4
4
  a result that should not be held in memory at once, `stream` runs the
5
5
  statement over the protocol's streaming opcode: the server answers with a
6
6
  header (the column names), then row chunks, then an end marker, and the
7
- driver yields rows as each chunk arrives, holding one chunk at a time.
7
+ driver yields rows as each chunk arrives, holding one chunk at a time. The
8
+ server executes a plain `SELECT` (bare columns from one table, unordered or
9
+ in storage-key order) page by page, and no scan budget applies to a
10
+ streamed page (server ≥ 0.330; `EXPLAIN` says whether a statement pages, and
11
+ the rules are under "Streamed queries" in the server's
12
+ [SQL syntax reference](https://skaidb.org/docs/query_syntax.html)).
8
13
 
9
14
  ```ruby
10
15
  conn.stream("SELECT id, payload FROM events ORDER BY id") do |row|
@@ -12,7 +17,9 @@ conn.stream("SELECT id, payload FROM events ORDER BY id") do |row|
12
17
  end
13
18
  ```
14
19
 
15
- - `stream(sql, consistency: nil)` takes SQL text only — no parameters.
20
+ - `stream(sql, params = [], consistency: nil)`: `params` bind `$1`, `$2`, …
21
+ exactly as `exec_params` binds them ([Parameters](#parameters), below);
22
+ `consistency:` overrides the connection's level for this statement.
16
23
  - With a block it returns a `Skaidb::Result` after the last row: `kind` is
17
24
  `:stream`, `fields` holds the column names (known even when no row
18
25
  arrived), and `rows` is empty because the rows went to the block.
@@ -21,9 +28,43 @@ end
21
28
  - A non-row statement (an `INSERT`, DDL, `USE`) answers with its ordinary
22
29
  result over the same opcode; `stream` returns that Result (`kind`
23
30
  `:mutation` with `cmd_tuples`, or `:ddl`) without yielding.
31
+ - A `CALL` whose procedure body `EMIT`s result sets answers with all of them
32
+ in one frame, as it does to `exec`: the block receives the rows `exec`
33
+ would return (the call's final result, the last set) and `stream` returns
34
+ that same `:rows` Result, every set in order under `result_sets`. Nothing
35
+ is left in flight, so the connection is free again before the first row
36
+ reaches the block.
24
37
  - A server without the streaming opcode raises `Skaidb::QueryError`
25
38
  (`server does not support streaming: …`).
26
39
 
40
+ ## Parameters
41
+
42
+ `params` bind the statement's `$1`, `$2`, … placeholders exactly as
43
+ `exec_params` binds them: typed, with every value [types.md](types.md) lists
44
+ (Arrays, Hashes, `Time`, `BigDecimal`, `Skaidb::Uuid`, …), the same `$N` as
45
+ often as you like, and the same checks before anything is sent (a
46
+ placeholder without a parameter, a parameter no placeholder uses, a bare
47
+ `?`).
48
+
49
+ ```ruby
50
+ conn.stream("SELECT id, ts, body FROM events WHERE ts >= $1 AND ts < $2", [from, to]) do |row|
51
+ export(row)
52
+ end
53
+
54
+ conn.stream("SELECT id, v FROM t WHERE tenant = $1", [tenant], consistency: :one).each_slice(1000) do |batch|
55
+ load(batch)
56
+ end
57
+ ```
58
+
59
+ They travel in `OP_QUERY_STREAM_PARAMS`, which needs a server ≥ 0.330. An
60
+ older server refuses it, and `stream` raises `Skaidb::QueryError`
61
+ (`server does not support parameterised streaming (skaidb >= 0.330): …`);
62
+ the connection stays usable. Unlike `exec_params` there is no client-side
63
+ text fallback. A stream without parameters (`nil` or `[]`) sends the
64
+ original `OP_QUERY_STREAM` with the SQL untouched, so it works against every
65
+ server. `params` must be an Array — anything else raises `ArgumentError`;
66
+ the consistency is only ever the `consistency:` keyword.
67
+
27
68
  ## Errors mid-stream
28
69
 
29
70
  The server can fail a statement after it has started sending rows (a node
data/docs/types.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # Types
2
2
 
3
3
  Results decode from skaidb's typed wire values; parameters encode back to
4
- them when a statement is prepared, which is the normal path whenever you pass
5
- values to `exec_params` or `exec_batch`.
4
+ them — when a statement is prepared, which is the normal path whenever you
5
+ pass values to `exec_params` or `exec_batch`, and always for the values you
6
+ pass to `stream`.
6
7
 
7
8
  | skaidb type | Result value | Accepted as a parameter |
8
9
  |---|---|---|
data/lib/skaidb.rb CHANGED
@@ -25,7 +25,8 @@
25
25
  # Placeholders use the pg-style +$1+, +$2+, ... . They are sent as TYPED values
26
26
  # through a server-side prepared statement where the server accepts one, and
27
27
  # interpolated into the SQL client-side, with correct quoting, where it does not
28
- # (old servers, and statement kinds that cannot be prepared).
28
+ # (old servers, and statement kinds that cannot be prepared). +stream+ sends
29
+ # its parameters typed in the stream request itself and never interpolates.
29
30
  require "socket"
30
31
  require "openssl"
31
32
  require "securerandom"
@@ -34,7 +35,7 @@ require "bigdecimal"
34
35
  module Skaidb
35
36
  # The package version — the single source of truth. The gemspec reads it,
36
37
  # and it is what the driver reports to the server in the Hello frame.
37
- VERSION = "1.2.0"
38
+ VERSION = "1.4.0"
38
39
 
39
40
  # Base class for every error raised by this driver.
40
41
  class Error < StandardError; end
@@ -930,6 +931,17 @@ module Skaidb
930
931
  # large scans.
931
932
  #
932
933
  # conn.stream("SELECT ...") { |row| puts row["id"] }
934
+ # conn.stream("SELECT id, ts FROM events WHERE ts >= $1 AND ts < $2", [from, to]) { |row| ... }
935
+ #
936
+ # +params+ bind +$1+, +$2+, ... exactly as +exec_params+ binds them —
937
+ # typed, the same values accepted (Arrays, Hashes, Times, ...), the same
938
+ # placeholder checks — and travel in OP_QUERY_STREAM_PARAMS, which needs
939
+ # a server >= 0.330: an older one answers QueryError "server does not
940
+ # support parameterised streaming (skaidb >= 0.330): ..." and the
941
+ # connection stays usable. Unlike +exec_params+ there is no client-side
942
+ # text fallback. Without parameters (+nil+ or +[]+) the request is the
943
+ # original OP_QUERY_STREAM with the SQL untouched, so a parameterless
944
+ # stream works against every server.
933
945
  #
934
946
  # The protocol forbids any other request on the connection until the
935
947
  # stream ends, so the whole exchange runs under the connection mutex
@@ -961,19 +973,30 @@ module Skaidb
961
973
  # +kind+ is +:stream+ and +fields+ holds the column names (known even when
962
974
  # no row arrived) while +rows+ stays empty — the rows went to the block;
963
975
  # a statement that produces no rows returns its ordinary +:mutation+ or
964
- # +:ddl+ Result without yielding.
965
- #
966
- # Takes no parameters — the streaming opcode carries SQL text.
967
- def stream(sql, consistency: nil)
968
- return enum_for(:stream, sql, consistency: consistency) unless block_given?
976
+ # +:ddl+ Result without yielding. A CALL whose body EMITs result sets is
977
+ # answered with all of them in one frame, streamed or not: the block
978
+ # receives the rows exec's Result would yield (the call's final result,
979
+ # the LAST set) and the return value is that same +:rows+ Result, every
980
+ # set under +result_sets+ — with nothing left in flight, so the block may
981
+ # use the connection.
982
+ def stream(sql, params = [], consistency: nil)
983
+ return enum_for(:stream, sql, params, consistency: consistency) unless block_given?
969
984
  raise ConnectionError, "connection is closed" if @closed
970
- ensure_live!
971
985
 
972
986
  level = consistency.nil? ? @consistency : Consistency.resolve(consistency)
973
- sql_bytes = sql.dup.force_encoding("UTF-8").b
974
- req = [5, level].pack("CC") + [sql_bytes.bytesize].pack("V") + sql_bytes
987
+ params ||= []
988
+ unless params.is_a?(Array)
989
+ raise ArgumentError, "stream params must be an Array, got #{params.class} " \
990
+ "(the consistency is a keyword: stream(sql, consistency: :one))"
991
+ end
992
+ # Built before anything touches the socket, so a parameter that cannot
993
+ # be bound is refused without a round trip (or a re-dial).
994
+ req = stream_request(sql, params, level)
995
+ ensure_live!
996
+
975
997
  live = false
976
998
  summary = nil
999
+ sets = nil
977
1000
  @mutex.synchronize do
978
1001
  started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
979
1002
  begin
@@ -989,11 +1012,21 @@ module Skaidb
989
1012
  case tag
990
1013
  when 3
991
1014
  msg = r.text
992
- raise QueryError, msg.include?("unknown opcode") ? "server does not support streaming: #{msg}" : msg
1015
+ if msg.include?("unknown opcode")
1016
+ what = params.empty? ? "streaming" : "parameterised streaming (skaidb >= 0.330)"
1017
+ raise QueryError, "server does not support #{what}: #{msg}"
1018
+ end
1019
+ raise QueryError, msg
993
1020
  when 1
994
1021
  return Result.new(fields: [], rows: [], cmd_tuples: r.u64, kind: :mutation) # not row-producing
995
1022
  when 2
996
1023
  return Result.new(fields: [], rows: [], cmd_tuples: 0, kind: :ddl)
1024
+ when 8
1025
+ # ResultSets: a CALL whose body EMITted is answered with every set
1026
+ # in ONE frame, streamed or not — the frame exec receives, decoded
1027
+ # by the same code. Nothing follows it, so the rows are yielded
1028
+ # below, with the connection free.
1029
+ sets = parse_result_sets(r)
997
1030
  when 5
998
1031
  # The server has already committed to a row stream, so from here
999
1032
  # the socket carries frames this call owns — mark it BEFORE
@@ -1044,6 +1077,12 @@ module Skaidb
1044
1077
  raise QueryError, "unexpected response tag #{tag} to stream request"
1045
1078
  end
1046
1079
  end
1080
+ if sets
1081
+ # What exec's Result yields (the call's final result, the LAST set),
1082
+ # and that same Result back, every set under +result_sets+.
1083
+ sets.each { |row| yield row }
1084
+ return sets
1085
+ end
1047
1086
  summary
1048
1087
  end
1049
1088
 
@@ -1311,6 +1350,30 @@ module Skaidb
1311
1350
  res
1312
1351
  end
1313
1352
 
1353
+ # The request +stream+ sends. Without parameters: OP_QUERY_STREAM with
1354
+ # the SQL as given, byte for byte what every server understands. With
1355
+ # them: OP_QUERY_STREAM_PARAMS (server >= 0.330), $N rewritten to the
1356
+ # positional ? and the values typed in ? order — the parameter vector
1357
+ # OP_EXECUTE carries (u16 count, then u32 length + value each).
1358
+ def stream_request(sql, params, level)
1359
+ if params.empty?
1360
+ sql_bytes = sql.dup.force_encoding("UTF-8").b
1361
+ return [5, level].pack("CC") + [sql_bytes.bytesize].pack("V") + sql_bytes
1362
+ end
1363
+
1364
+ qsql, order = Skaidb.to_qmark(sql.to_s, params)
1365
+ # The count is a u16 on the wire, and pack would wrap a larger one.
1366
+ raise QueryError, "a statement takes at most 65535 parameters, got #{order.length}" if order.length > 0xFFFF
1367
+
1368
+ sql_bytes = qsql.dup.force_encoding("UTF-8").b
1369
+ req = +([9, level].pack("CC") + [sql_bytes.bytesize].pack("V") + sql_bytes + [order.length].pack("v"))
1370
+ order.each do |p|
1371
+ b = Skaidb.encode_value(p)
1372
+ req << [b.bytesize].pack("V") << b
1373
+ end
1374
+ req
1375
+ end
1376
+
1314
1377
  # One request and its response under the connection mutex. In a pool the
1315
1378
  # round trip (send to the first response frame) feeds the member's
1316
1379
  # latency average, and a transport failure counts against the member.
@@ -1343,27 +1406,9 @@ module Skaidb
1343
1406
  tag = r.u8
1344
1407
  case tag
1345
1408
  when 0 # Rows
1346
- ncols = r.u32
1347
- columns = Array.new(ncols) { r.text }
1348
- nrows = r.u32
1349
- rows = Array.new(nrows) do
1350
- ncells = r.u32
1351
- Array.new(ncells) { Skaidb.decode_value(Reader.new(r.blob)) }
1352
- end
1353
- Result.new(fields: columns, rows: rows, cmd_tuples: 0)
1409
+ parse_rows(r)
1354
1410
  when 8 # ResultSets: a CALL whose body EMITted
1355
- sets = Array.new(r.u32) do
1356
- ncols = r.u32
1357
- columns = Array.new(ncols) { r.text }
1358
- nrows = r.u32
1359
- rows = Array.new(nrows) do
1360
- ncells = r.u32
1361
- Array.new(ncells) { Skaidb.decode_value(Reader.new(r.blob)) }
1362
- end
1363
- Result.new(fields: columns, rows: rows, cmd_tuples: 0)
1364
- end
1365
- last = sets.last || Result.new(fields: [], rows: [], cmd_tuples: 0)
1366
- Result.new(fields: last.fields, rows: last.rows, cmd_tuples: 0, result_sets: sets)
1411
+ parse_result_sets(r)
1367
1412
  when 1 # Mutation
1368
1413
  Result.new(fields: [], rows: [], cmd_tuples: r.u64, kind: :mutation)
1369
1414
  when 2 # Ddl
@@ -1374,6 +1419,29 @@ module Skaidb
1374
1419
  raise ConnectionError, "unknown response tag #{tag}"
1375
1420
  end
1376
1421
  end
1422
+
1423
+ # A Rows body (PROTOCOL.md §3.2), also one set of ResultSets; +r+ sits
1424
+ # just past the tag.
1425
+ def parse_rows(r)
1426
+ ncols = r.u32
1427
+ columns = Array.new(ncols) { r.text }
1428
+ nrows = r.u32
1429
+ rows = Array.new(nrows) do
1430
+ ncells = r.u32
1431
+ Array.new(ncells) { Skaidb.decode_value(Reader.new(r.blob)) }
1432
+ end
1433
+ Result.new(fields: columns, rows: rows, cmd_tuples: 0)
1434
+ end
1435
+
1436
+ # A ResultSets body (a CALL whose body EMITted), +r+ just past the tag:
1437
+ # the LAST set is the Result's own rows, every set is under
1438
+ # +result_sets+. The buffered path and a stream request both receive
1439
+ # this frame, and both decode it here.
1440
+ def parse_result_sets(r)
1441
+ sets = Array.new(r.u32) { parse_rows(r) }
1442
+ last = sets.last || Result.new(fields: [], rows: [], cmd_tuples: 0)
1443
+ Result.new(fields: last.fields, rows: last.rows, cmd_tuples: 0, result_sets: sets)
1444
+ end
1377
1445
  end
1378
1446
 
1379
1447
  # ---- module entry point --------------------------------------------------
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skaidb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - skaidb contributors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-25 00:00:00.000000000 Z
11
+ date: 2026-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal