skaidb 1.2.0 → 1.3.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: a452610f682221a59cfbbc48e42c9a37f1c0e880d2191037f1c9c7151a3034fa
4
+ data.tar.gz: 1a73f7f552091bfaed9b4072dfd684b57daac637ff12d163a34bf1c278bef784
5
5
  SHA512:
6
- metadata.gz: 9ce5348688c5bd6cd480006b2fa567ed7af74cc0ffe557f387d55705effd49dcbd754b8c52b32a30ae3361234226e4a82828bff7dc0d3d43f9a03e086849c53b
7
- data.tar.gz: e7d6fd76c93392bbea0e2583b5741b418188c4d045d0226d7e8d0dcf2828fbe4cd03e34a32da58598d0173d81875cf8b1fe5b405c64e0ac658c108be67adf1da
6
+ metadata.gz: ce44a1e28e0add382aa9fde578eb09350e1b5306d22c3cf5d272faa5d606c8ec05be08b77967d014a33ceddba5fefd0ed449503c24d4997680c37af156ad2e5f
7
+ data.tar.gz: 3d34175d862982988e593fb42c631bdbb9cd6f23682908ec40fc53a7d25fe3868105201dd50988108508783bfb8bf076065737864d71c6cc5337c8b6c371e6c3
data/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@ 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.3.0] - 2026-10-29
8
+
9
+ ### Added
10
+ - **Parameterised streaming**: `conn.stream(sql, params)` binds `$1`, `$2`,
11
+ … exactly as `exec_params(sql, params)` does — typed, with the same values
12
+ (Arrays, Hashes, `Time`, `BigDecimal`, `Skaidb::Uuid`, …) and the same
13
+ placeholder checks — over `OP_QUERY_STREAM_PARAMS` (server ≥ 0.330), so a
14
+ range-bounded export no longer needs its values written into the SQL. It
15
+ takes `consistency:`, a block or the Enumerator form like any stream.
16
+ Without parameters (`nil` or `[]`) the request is the original
17
+ `OP_QUERY_STREAM`, byte for byte, so it works against every server; a
18
+ parameterised stream against an older server raises `QueryError`
19
+ (`server does not support parameterised streaming (skaidb >= 0.330): …`)
20
+ and leaves the connection usable. `params` that are not an Array raise
21
+ `ArgumentError`.
22
+ - The vendored conformance suite's `stream_params` case (a streamed
23
+ statement with bound parameters) runs through `stream(sql, params)`.
24
+
7
25
  ## [1.2.0] - 2026-10-25
8
26
 
9
27
  ### Added
@@ -174,6 +192,7 @@ batches, a 2 500-row stream, error handling, and the `drivers` table showing
174
192
  because it is a bundled gem since Ruby 3.4.
175
193
  - The example moved to `examples/basic.rb`.
176
194
 
195
+ [1.3.0]: https://github.com/porcupin26/skaidb-ruby/compare/v1.2.0...v1.3.0
177
196
  [1.2.0]: https://github.com/porcupin26/skaidb-ruby/compare/v1.1.0...v1.2.0
178
197
  [1.1.0]: https://github.com/porcupin26/skaidb-ruby/compare/v1.0.3...v1.1.0
179
198
  [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.3.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.3.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,23 @@ 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.
298
310
 
299
311
  **The abandon/drain rule.** The connection is busy for the whole stream:
300
312
  a statement from another thread waits for it. Leaving the block early —
@@ -418,8 +430,8 @@ StandardError
418
430
  - A transport failure (`ConnectionError`) marks the connection broken; the
419
431
  next statement re-dials, or a pool replaces it.
420
432
  - `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.
433
+ `load_balancing`/`idle_order`, for `Skaidb::Uuid.new` of a string that
434
+ is not a UUID, and for `stream` parameters that are not an Array.
423
435
 
424
436
  ## Transactions
425
437
 
@@ -446,7 +458,7 @@ frame after every handshake. It shows up as `client_name = 'ruby'` /
446
458
 
447
459
  ```sql
448
460
  SELECT client_name, client_version FROM drivers;
449
- -- ruby | 1.2.0
461
+ -- ruby | 1.3.0
450
462
  ```
451
463
 
452
464
  The server records the row asynchronously, so a `SELECT` immediately after
@@ -461,7 +473,8 @@ know the opcode answers with an error the driver ignores.
461
473
  falls back: prepared statements → client-side text binding, `Hello` →
462
474
  ignored, pool discovery (`SHOW ENDPOINTS`, server ≥ 0.327) → the
463
475
  configured seeds. `stream` and `exec_batch` need a server with those
464
- opcodes and raise `QueryError` otherwise.
476
+ opcodes, and `stream` with parameters a server ≥ 0.330; they raise
477
+ `QueryError` otherwise.
465
478
  - The wire protocol (framing, SCRAM handshake, value encoding, opcodes) is
466
479
  specified at <https://skaidb.org/docs/PROTOCOL.html>. The driver speaks the
467
480
  binary protocol on port 7000; the server's REST/JSON gateway (port 7080)
data/docs/api.md CHANGED
@@ -55,8 +55,16 @@ 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
70
  Result without yielding. Without a block returns an Enumerator.
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.
@@ -24,6 +31,34 @@ end
24
31
  - A server without the streaming opcode raises `Skaidb::QueryError`
25
32
  (`server does not support streaming: …`).
26
33
 
34
+ ## Parameters
35
+
36
+ `params` bind the statement's `$1`, `$2`, … placeholders exactly as
37
+ `exec_params` binds them: typed, with every value [types.md](types.md) lists
38
+ (Arrays, Hashes, `Time`, `BigDecimal`, `Skaidb::Uuid`, …), the same `$N` as
39
+ often as you like, and the same checks before anything is sent (a
40
+ placeholder without a parameter, a parameter no placeholder uses, a bare
41
+ `?`).
42
+
43
+ ```ruby
44
+ conn.stream("SELECT id, ts, body FROM events WHERE ts >= $1 AND ts < $2", [from, to]) do |row|
45
+ export(row)
46
+ end
47
+
48
+ conn.stream("SELECT id, v FROM t WHERE tenant = $1", [tenant], consistency: :one).each_slice(1000) do |batch|
49
+ load(batch)
50
+ end
51
+ ```
52
+
53
+ They travel in `OP_QUERY_STREAM_PARAMS`, which needs a server ≥ 0.330. An
54
+ older server refuses it, and `stream` raises `Skaidb::QueryError`
55
+ (`server does not support parameterised streaming (skaidb >= 0.330): …`);
56
+ the connection stays usable. Unlike `exec_params` there is no client-side
57
+ text fallback. A stream without parameters (`nil` or `[]`) sends the
58
+ original `OP_QUERY_STREAM` with the SQL untouched, so it works against every
59
+ server. `params` must be an Array — anything else raises `ArgumentError`;
60
+ the consistency is only ever the `consistency:` keyword.
61
+
27
62
  ## Errors mid-stream
28
63
 
29
64
  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.3.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
@@ -962,16 +974,21 @@ module Skaidb
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
976
  # +: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?
977
+ def stream(sql, params = [], consistency: nil)
978
+ return enum_for(:stream, sql, params, consistency: consistency) unless block_given?
969
979
  raise ConnectionError, "connection is closed" if @closed
970
- ensure_live!
971
980
 
972
981
  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
982
+ params ||= []
983
+ unless params.is_a?(Array)
984
+ raise ArgumentError, "stream params must be an Array, got #{params.class} " \
985
+ "(the consistency is a keyword: stream(sql, consistency: :one))"
986
+ end
987
+ # Built before anything touches the socket, so a parameter that cannot
988
+ # be bound is refused without a round trip (or a re-dial).
989
+ req = stream_request(sql, params, level)
990
+ ensure_live!
991
+
975
992
  live = false
976
993
  summary = nil
977
994
  @mutex.synchronize do
@@ -989,7 +1006,11 @@ module Skaidb
989
1006
  case tag
990
1007
  when 3
991
1008
  msg = r.text
992
- raise QueryError, msg.include?("unknown opcode") ? "server does not support streaming: #{msg}" : msg
1009
+ if msg.include?("unknown opcode")
1010
+ what = params.empty? ? "streaming" : "parameterised streaming (skaidb >= 0.330)"
1011
+ raise QueryError, "server does not support #{what}: #{msg}"
1012
+ end
1013
+ raise QueryError, msg
993
1014
  when 1
994
1015
  return Result.new(fields: [], rows: [], cmd_tuples: r.u64, kind: :mutation) # not row-producing
995
1016
  when 2
@@ -1311,6 +1332,30 @@ module Skaidb
1311
1332
  res
1312
1333
  end
1313
1334
 
1335
+ # The request +stream+ sends. Without parameters: OP_QUERY_STREAM with
1336
+ # the SQL as given, byte for byte what every server understands. With
1337
+ # them: OP_QUERY_STREAM_PARAMS (server >= 0.330), $N rewritten to the
1338
+ # positional ? and the values typed in ? order — the parameter vector
1339
+ # OP_EXECUTE carries (u16 count, then u32 length + value each).
1340
+ def stream_request(sql, params, level)
1341
+ if params.empty?
1342
+ sql_bytes = sql.dup.force_encoding("UTF-8").b
1343
+ return [5, level].pack("CC") + [sql_bytes.bytesize].pack("V") + sql_bytes
1344
+ end
1345
+
1346
+ qsql, order = Skaidb.to_qmark(sql.to_s, params)
1347
+ # The count is a u16 on the wire, and pack would wrap a larger one.
1348
+ raise QueryError, "a statement takes at most 65535 parameters, got #{order.length}" if order.length > 0xFFFF
1349
+
1350
+ sql_bytes = qsql.dup.force_encoding("UTF-8").b
1351
+ req = +([9, level].pack("CC") + [sql_bytes.bytesize].pack("V") + sql_bytes + [order.length].pack("v"))
1352
+ order.each do |p|
1353
+ b = Skaidb.encode_value(p)
1354
+ req << [b.bytesize].pack("V") << b
1355
+ end
1356
+ req
1357
+ end
1358
+
1314
1359
  # One request and its response under the connection mutex. In a pool the
1315
1360
  # round trip (send to the first response frame) feeds the member's
1316
1361
  # latency average, and a transport failure counts against the member.
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.3.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-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal