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 +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +28 -12
- data/docs/api.md +14 -3
- data/docs/pooling.md +6 -0
- data/docs/streaming.md +43 -2
- data/docs/types.md +3 -2
- data/lib/skaidb.rb +99 -31
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f947092b55d66cc8d7c55153cbea09ebcdd0298f48fcb6c97ea0f3a5edd5ac01
|
|
4
|
+
data.tar.gz: 9b472d16803f161f2aa6d4554c19aede65f9a227b79d29c7f015204c04a7bcc7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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
|
|
284
|
-
opcode and yields one row Hash at a time, holding **one chunk** in
|
|
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
|
-
`
|
|
295
|
-
|
|
296
|
-
(
|
|
297
|
-
|
|
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`,
|
|
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.
|
|
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
|
|
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).
|
|
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
|
|
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)`
|
|
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
|
|
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.
|
|
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
|
-
#
|
|
967
|
-
|
|
968
|
-
|
|
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
|
-
|
|
974
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
11
|
+
date: 2026-10-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bigdecimal
|