skaidb 1.0.3

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.
data/README.md ADDED
@@ -0,0 +1,484 @@
1
+ # skaidb — Ruby driver
2
+
3
+ [![CI](https://github.com/porcupin26/skaidb-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/porcupin26/skaidb-ruby/actions/workflows/ci.yml)
4
+ [![Release](https://img.shields.io/github/v/release/porcupin26/skaidb-ruby?label=release)](https://github.com/porcupin26/skaidb-ruby/releases/latest)
5
+ [![Gem](https://img.shields.io/gem/v/skaidb?label=rubygems)](https://rubygems.org/gems/skaidb)
6
+ [![License: SSPL-1.0](https://img.shields.io/badge/license-SSPL--1.0-blue.svg)](https://github.com/porcupin26/skaidb-ruby/blob/main/LICENSE)
7
+
8
+ The official Ruby driver for [skaidb](https://skaidb.org). The API is modelled
9
+ on the [ruby-pg](https://rubygems.org/gems/pg) gem: `Skaidb.connect` returns a
10
+ connection, `exec` / `exec_params` run statements with `$1`-style parameters,
11
+ and the result behaves like `PG::Result`. **Pure standard library** (`socket`,
12
+ `openssl`, `securerandom`, `bigdecimal`) — no third-party code, one file,
13
+ Ruby 2.7 through 3.4. It speaks skaidb's binary protocol directly:
14
+ SCRAM-SHA-256 authentication, server-side prepared statements with typed
15
+ parameters, one-round-trip batches, streamed result sets, multi-seed failover,
16
+ transparent reconnect, TLS and connection pooling.
17
+
18
+ - Repository: <https://github.com/porcupin26/skaidb-ruby>
19
+ - RubyGems: <https://rubygems.org/gems/skaidb>
20
+ - Full reference: [`docs/`](https://github.com/porcupin26/skaidb-ruby/tree/main/docs) —
21
+ [getting started](https://github.com/porcupin26/skaidb-ruby/blob/main/docs/getting-started.md),
22
+ [API reference](https://github.com/porcupin26/skaidb-ruby/blob/main/docs/api.md),
23
+ [types](https://github.com/porcupin26/skaidb-ruby/blob/main/docs/types.md),
24
+ [TLS](https://github.com/porcupin26/skaidb-ruby/blob/main/docs/tls.md),
25
+ [streaming](https://github.com/porcupin26/skaidb-ruby/blob/main/docs/streaming.md),
26
+ [pooling](https://github.com/porcupin26/skaidb-ruby/blob/main/docs/pooling.md),
27
+ [changelog](https://github.com/porcupin26/skaidb-ruby/blob/main/CHANGELOG.md)
28
+ - Server documentation: <https://skaidb.org/docs/>
29
+ - Wire protocol the driver speaks: <https://skaidb.org/docs/PROTOCOL.html>
30
+
31
+ ## Install
32
+
33
+ The gem is published on RubyGems.org as
34
+ [`skaidb`](https://rubygems.org/gems/skaidb) and is loaded with
35
+ `require "skaidb"`:
36
+
37
+ ```sh
38
+ gem install skaidb
39
+ ```
40
+
41
+ With Bundler:
42
+
43
+ ```ruby
44
+ # Gemfile
45
+ gem "skaidb", "~> 1.0"
46
+ ```
47
+
48
+ Bundler resolves the gem's one dependency, `bigdecimal` (part of Ruby's
49
+ standard library, declared because it is a bundled rather than default gem
50
+ from Ruby 3.4), from RubyGems.org and compiles it, even on a Ruby that ships
51
+ it; that needs the Ruby headers and a C toolchain (`ruby-dev` and
52
+ `build-essential` on Debian/Ubuntu, `ruby-devel` and `gcc` on Fedora/RHEL,
53
+ Xcode command-line tools on macOS). `gem install skaidb` and vendoring reuse
54
+ the `bigdecimal` already installed.
55
+
56
+ Or vendor the single file: copy
57
+ [`lib/skaidb.rb`](https://github.com/porcupin26/skaidb-ruby/blob/v1.0.3/lib/skaidb.rb)
58
+ into your project and `require_relative` it. It has no dependencies beyond
59
+ Ruby's standard library.
60
+
61
+ Every release is also attached as a `.gem` to its
62
+ [GitHub release](https://github.com/porcupin26/skaidb-ruby/releases) for
63
+ installs without registry access: `gem install ./skaidb-1.0.3.gem`.
64
+
65
+ ## Quick start
66
+
67
+ ```ruby
68
+ require "skaidb"
69
+
70
+ conn = Skaidb.connect(host: "localhost", port: 7000,
71
+ user: "skaidb", password: "secret", database: "app")
72
+
73
+ conn.exec("CREATE TABLE users (PRIMARY KEY (id))")
74
+ conn.exec_params("INSERT INTO users (id, name, tags) VALUES ($1, $2, $3)",
75
+ [1, "Ada", ["math", "eng"]])
76
+
77
+ res = conn.exec_params("SELECT id, name, tags FROM users WHERE id = $1", [1])
78
+ res.each { |row| puts row["name"] } # Ada
79
+ p res.fields # ["id", "name", "tags"]
80
+ p res.rows # [[1, "Ada", ["math", "eng"]]]
81
+
82
+ conn.close
83
+ ```
84
+
85
+ With a block the connection is closed for you:
86
+
87
+ ```ruby
88
+ Skaidb.connect(host: "localhost", user: "skaidb", password: "secret") do |conn|
89
+ conn.exec("SELECT id, name FROM users ORDER BY id").each { |row| p row }
90
+ end
91
+ ```
92
+
93
+ ## Table of contents
94
+
95
+ - [Connecting](#connecting) — host/port, seeds and failover, `database:`,
96
+ timeout, TLS, consistency, health
97
+ - [Statements and parameters](#statements-and-parameters) — `$1` binding,
98
+ prepared statements, `exec_batch`
99
+ - [Results](#results)
100
+ - [Streaming large results](#streaming-large-results)
101
+ - [Connection pool](#connection-pool)
102
+ - [Streams (`CREATE STREAM`)](#streams-create-stream) — `subscribe`
103
+ - [Type mapping](#type-mapping)
104
+ - [Errors](#errors)
105
+ - [Transactions](#transactions)
106
+ - [Thread safety](#thread-safety)
107
+ - [Client identification and version](#client-identification-and-version)
108
+ - [Compatibility](#compatibility)
109
+
110
+ ## Connecting
111
+
112
+ ```ruby
113
+ Skaidb.connect(
114
+ host: "localhost", port: 7000,
115
+ user: "anonymous", password: "",
116
+ consistency: :quorum, # :one | :quorum | :all (also "ONE"… or 0/1/2)
117
+ timeout: 10.0, # TCP connect timeout in seconds; nil = none
118
+ database: nil, # run USE <database> as part of connecting
119
+ seeds: nil, # ["db1", "db2:7000", …] tried in random order
120
+ tls: false, tls_ca: nil, tls_insecure: false, tls_server_name: "skaidb"
121
+ ) # => Skaidb::Connection (or the block's value when a block is given)
122
+ ```
123
+
124
+ `connect` dials, runs the SCRAM-SHA-256 handshake (with mutual
125
+ authentication — the server proves it knows your password too), sends a
126
+ best-effort `Hello` naming the driver and its version, and runs `USE` if a
127
+ `database` was given. Omit `user`/`password` for a server with authentication
128
+ disabled.
129
+
130
+ ### Seeds and failover
131
+
132
+ skaidb is leaderless: every node accepts every read and write. Pass the
133
+ cluster's addresses as `seeds`; they are tried in **randomized order** until
134
+ one connects *and* authenticates, which also spreads a fleet of clients across
135
+ the nodes.
136
+
137
+ ```ruby
138
+ conn = Skaidb.connect(seeds: ["db1", "db2:7000", "db3"], database: "app")
139
+ ```
140
+
141
+ Each seed is `"host"` or `"host:port"` (the port after the last colon wins; a
142
+ seed without a port uses `port:`). When a connection's transport dies, the
143
+ **next statement re-dials** across the same seeds, re-authenticates, re-sends
144
+ `Hello`, re-runs `USE`, and drops the connection's prepared statements — a
145
+ recovered connection is indistinguishable from a fresh one. The statement that
146
+ discovered the loss raises `Skaidb::ConnectionError`; the one after it goes
147
+ through.
148
+
149
+ ### Timeout
150
+
151
+ `timeout` bounds the TCP dial only. Reads have no deadline: a peer that is
152
+ alive but silent blocks the caller, as in ruby-pg. `close` deliberately does
153
+ not take the connection's lock, so another thread can break a stuck read by
154
+ closing the connection.
155
+
156
+ ### TLS
157
+
158
+ ```ruby
159
+ conn = Skaidb.connect(host: "db1", tls: true) # system trust store
160
+ conn = Skaidb.connect(host: "db1", tls_ca: "/etc/skaidb/ca.crt") # the cluster CA
161
+ conn = Skaidb.connect(host: "db1", tls_insecure: true) # dev only: no verification
162
+ ```
163
+
164
+ Any of the three enables TLS. The server name sent as SNI and checked against
165
+ the certificate is `tls_server_name` (default `"skaidb"`, the SAN skaidb's own
166
+ certificates carry). SCRAM runs inside the TLS session. The driver does not
167
+ present a client certificate. Details: [docs/tls.md](https://github.com/porcupin26/skaidb-ruby/blob/main/docs/tls.md).
168
+
169
+ ### Consistency
170
+
171
+ skaidb has tunable consistency; the driver's default is `:quorum`. Set it per
172
+ connection and override it per statement:
173
+
174
+ ```ruby
175
+ conn = Skaidb.connect(host: "db1", consistency: :one)
176
+ conn.consistency = :all # subsequent statements
177
+ conn.exec_params("SELECT …", [], consistency: :one) # this statement only
178
+ conn.exec_batch("INSERT …", rows, consistency: :quorum)
179
+ conn.stream("SELECT …", consistency: :one) { |row| … }
180
+ ```
181
+
182
+ `Skaidb::Consistency::ONE / QUORUM / ALL` are the integers `0 / 1 / 2`;
183
+ `Skaidb::Consistency.resolve` accepts a Symbol, a String (any case) or the
184
+ integer and raises `Skaidb::Error` otherwise. DDL is always run at quorum by
185
+ the server regardless of this setting.
186
+
187
+ ### Health
188
+
189
+ - `conn.usable?` — no round-trip. `true` unless the connection is closed, was
190
+ left out of sync by a transport error or an undrainable abandoned stream, or
191
+ is currently mid-stream.
192
+ - `conn.closed` / `conn.finished?` — `true` after `close`.
193
+
194
+ ## Statements and parameters
195
+
196
+ Placeholders are pg-style `$1`, `$2`, …; pass parameters as an Array:
197
+
198
+ ```ruby
199
+ conn.exec_params("SELECT * FROM users WHERE name = $1 AND age > $2", ["O'Brien", 30])
200
+ ```
201
+
202
+ `exec(sql)` (alias `query`) runs a statement with no parameters. The same
203
+ `$N` may appear more than once. A bare `?` in a statement that has parameters
204
+ is rejected up front with a message saying the driver's syntax is `$1, $2, …`
205
+ (the server would otherwise fail it late with a confusing arity error).
206
+
207
+ ### How binding works
208
+
209
+ A parameterized statement is **prepared on the server** and its values are
210
+ sent as typed values over the binary protocol. Nothing is interpolated into
211
+ SQL text, so `"O'Brien"` needs no escaping, and a parameter can be an Array
212
+ (→ Array) or a Hash (→ Document), which have no SQL literal form:
213
+
214
+ ```ruby
215
+ conn.exec_params("INSERT INTO docs (id, meta) VALUES ($1, $2)",
216
+ [7, { "city" => "London", "tags" => ["a", "b"] }])
217
+ conn.exec_params("SELECT id FROM users WHERE id IN ($1)", [[1, 2, 3]]) # set membership
218
+ ```
219
+
220
+ Prepared statements are cached per connection (up to 240 entries, under the
221
+ server's 256-per-connection limit) and reused. The cache is dropped when the
222
+ connection re-dials.
223
+
224
+ Statement kinds the server refuses to prepare (DDL and session control such
225
+ as `USE`) fall back to **client-side text binding** for scalar parameters: a
226
+ String is quoted with `''` escaping, a `Time` becomes epoch milliseconds, a
227
+ binary String becomes a hex literal, and Arrays/Hashes raise `QueryError` on
228
+ this path — with the server's reason for refusing the prepare appended, since
229
+ that is usually the real mistake. Mismatched placeholder/parameter counts
230
+ raise `QueryError` on either path.
231
+
232
+ ### `exec_batch` — bulk writes in one round-trip
233
+
234
+ ```ruby
235
+ n = conn.exec_batch("INSERT INTO t (id, v) VALUES ($1, $2)", [[1, "a"], [2, "b"], [3, "c"]])
236
+ n # => 3, total rows affected
237
+ ```
238
+
239
+ The statement is prepared once and **every parameter row ships in a single
240
+ frame** (`ExecuteBatch`). Each row autocommits on its own; on a failure the
241
+ `QueryError` names the row index and earlier rows stay applied, so the
242
+ statement should be idempotent. A statement the server cannot prepare raises
243
+ `QueryError` rather than falling back. An empty row list returns 0 without a
244
+ round-trip.
245
+
246
+ ## Results
247
+
248
+ `exec`, `query`, `exec_params` and `exec_prepared` return a `Skaidb::Result`,
249
+ shaped like `PG::Result`:
250
+
251
+ - `fields` (alias `columns`): column names in order; `nfields` / `num_fields`.
252
+ - `rows`: rows as Arrays of values; `ntuples` / `num_tuples`.
253
+ - `each` / `Enumerable` / `values` / `to_a`: rows as Hashes keyed by column
254
+ name (String keys); `res[i]` is row `i` as a Hash; `getvalue(row, col)` one
255
+ cell by index or column name.
256
+ - `cmd_tuples`: rows affected by `INSERT`/`UPDATE`/`DELETE`; `0` for a
257
+ `SELECT` and for DDL.
258
+ - `result_sets`: for a `CALL` whose procedure `EMIT`s several result sets,
259
+ every set in order (each a `Result`); the last set is the result's own
260
+ `rows`/`fields`. Empty for an ordinary reply.
261
+
262
+ A result holds all its rows in memory; for results that do not fit, stream.
263
+
264
+ ## Streaming large results
265
+
266
+ `conn.stream(sql, consistency: nil)` runs the statement over the streaming
267
+ opcode and yields one row Hash at a time, holding **one chunk** in memory:
268
+
269
+ ```ruby
270
+ conn.stream("SELECT id, pad FROM big ORDER BY id") do |row|
271
+ break if enough?(row) # leaving early drains the tail
272
+ end
273
+
274
+ first = conn.stream("SELECT id FROM big ORDER BY id").first(10) # Enumerator form
275
+ ```
276
+
277
+ `stream` takes SQL text only (no parameters). A non-row statement returns
278
+ `nil` without yielding.
279
+
280
+ **The abandon/drain rule.** The connection is busy for the whole stream:
281
+ a statement from another thread waits for it. Leaving the block early —
282
+ `break`, `return`, an exception, or any Enumerable method on the Enumerator
283
+ form — drains the frames the server is still sending so the socket sits at a
284
+ request boundary again. Draining transfers the rest of the result: if you only
285
+ want the first rows, say so with `LIMIT`. External iteration (`next`/`peek`)
286
+ on the Enumerator form cannot unwind and leaves the connection marked busy;
287
+ iterate with a block or `each`. A frame that makes no sense mid-stream, or a
288
+ dead socket, marks the connection broken instead; `usable?` turns `false`
289
+ and the next statement re-dials. Details: [docs/streaming.md](https://github.com/porcupin26/skaidb-ruby/blob/main/docs/streaming.md).
290
+
291
+ ## Connection pool
292
+
293
+ ```ruby
294
+ pool = Skaidb::Pool.new(seeds: ["db1", "db2", "db3"], database: "app", maxsize: 8)
295
+
296
+ pool.with { |conn| conn.exec_params("SELECT … WHERE id IN ($1)", [[1, 2, 3]]) }
297
+
298
+ conn = pool.checkout; …; pool.checkin(conn) # the explicit form
299
+ pool.close # closes idle connections
300
+ ```
301
+
302
+ `Skaidb::Pool` is thread-safe and accepts every `Skaidb.connect` keyword.
303
+ `maxsize` bounds the number of **idle** connections retained; checkout never
304
+ blocks — when no idle connection is available a new one is dialed, and a
305
+ returned connection beyond `maxsize` is closed. Connections are validated with
306
+ `usable?` on checkout and check-in, so one broken by a transport error or an
307
+ undrained stream is closed and replaced. Details: [docs/pooling.md](https://github.com/porcupin26/skaidb-ruby/blob/main/docs/pooling.md).
308
+
309
+ ## Streams (`CREATE STREAM`)
310
+
311
+ `conn.subscribe(name, after: nil, poll: 0.5)` yields a stream's events forever
312
+ as Hashes with `"id"`, `"op"`, `"k"`, `"ts"`, `"doc"`. It polls the stream's
313
+ log with a keyset cursor (500 events per page, sleeping `poll` seconds when
314
+ caught up), so it needs no MQTT client. `id` is an opaque String that sorts in
315
+ log order — persist the last one you handled and pass it as `after:` to resume
316
+ exactly there.
317
+
318
+ ```ruby
319
+ conn.subscribe("big_orders", after: checkpoint) do |ev|
320
+ handle(ev["doc"])
321
+ checkpoint = ev["id"]
322
+ end
323
+ ```
324
+
325
+ For push delivery, subscribe to `$stream/<db>/<name>` with any MQTT client
326
+ instead; the events are identical.
327
+
328
+ ## Type mapping
329
+
330
+ | skaidb value | Ruby → bind | Ruby ← result |
331
+ |--------------|-----------------------------------------------|-------------------------------------|
332
+ | Null | `nil` | `nil` |
333
+ | Bool | `true` / `false` | `true` / `false` |
334
+ | Int | `Integer` (signed 64-bit) | `Integer` |
335
+ | Float | `Float` (finite only) | `Float` |
336
+ | Decimal | `BigDecimal` (finite, 128-bit mantissa) | `BigDecimal` |
337
+ | String | `String` (any text encoding), `Symbol` | `String` (UTF-8) |
338
+ | Bytes | `String` with `Encoding::BINARY` (`"…".b`) | `String` with `Encoding::BINARY` |
339
+ | Uuid | `Skaidb::Uuid` | `String`, canonical lowercase |
340
+ | Timestamp | `Time` (millisecond precision) | `Time` (UTC) |
341
+ | Array | `Array` | `Array` |
342
+ | Document | `Hash` (keys become Strings) | `Hash`, keys in server order |
343
+
344
+ Worth knowing:
345
+
346
+ - A `String` binds as Bytes when its encoding is `ASCII-8BIT`/`BINARY` and as
347
+ String otherwise, so read a file with `File.binread` or call `.b` to store
348
+ raw bytes, and keep text in UTF-8.
349
+ - A UUID result is a String, so `"…" == row["id"]` works; to *bind* one with
350
+ the Uuid type wrap it: `Skaidb::Uuid.new("6ba7b810-…")`, `Skaidb::Uuid.random`,
351
+ `Skaidb::Uuid.from_bytes(raw16)`. A `Uuid` compares equal to a String in the
352
+ same canonical form.
353
+ - `Time` keeps its instant whatever its zone; sub-millisecond precision is
354
+ truncated. Results are UTC.
355
+ - A Document result is a `Hash` whose keys come back in the order the server
356
+ sends them, which is not the order they were bound in: the server stores a
357
+ document in canonical form, with keys sorted at every level. Do not rely
358
+ on key order.
359
+ - An `Integer` outside the signed 64-bit range, a `NaN`/`Infinity` Float, a
360
+ non-finite `BigDecimal` or one whose digits exceed a signed 128-bit
361
+ mantissa, and any other type (`Date`, `Rational`, …) raise `QueryError` —
362
+ convert them yourself.
363
+
364
+ Details: [docs/types.md](https://github.com/porcupin26/skaidb-ruby/blob/main/docs/types.md).
365
+
366
+ ## Errors
367
+
368
+ ```
369
+ StandardError
370
+ └── Skaidb::Error # also: invalid consistency, pool closed
371
+ ├── Skaidb::ConnectionError # dial/auth/transport failures, framing, closed connection
372
+ └── Skaidb::QueryError # the server rejected the statement; bad parameters
373
+ ```
374
+
375
+ - A server `Error` frame is a **statement** error (`QueryError`); the
376
+ connection stays usable.
377
+ - A transport failure (`ConnectionError`) marks the connection broken; the
378
+ next statement re-dials, or a pool replaces it.
379
+ - `ArgumentError` for `Pool.new(maxsize: 0)` and for `Skaidb::Uuid.new` of a
380
+ string that is not a UUID.
381
+
382
+ ## Transactions
383
+
384
+ skaidb autocommits every statement. There is no `BEGIN` on this driver's API;
385
+ where your server supports statement-level transaction control, issue
386
+ `BEGIN`/`COMMIT`/`ROLLBACK` as ordinary statements. **On a cluster,
387
+ transaction control is not available** — a `BEGIN` there is refused by the
388
+ server as a statement error. See the server documentation for what your
389
+ deployment supports.
390
+
391
+ ## Thread safety
392
+
393
+ Threads may share the module but should not share a connection. A connection
394
+ serializes its own round-trips with a Mutex, and a stream claims it outright,
395
+ but the intended shape for concurrency is one connection per thread, which is
396
+ what a [pool](#connection-pool) gives you.
397
+
398
+ ## Client identification and version
399
+
400
+ `Skaidb::VERSION` is the package version — defined once in `lib/skaidb.rb`,
401
+ read by the gemspec, and what the driver reports to the server in the `Hello`
402
+ frame after every handshake. It shows up as `client_name = 'ruby'` /
403
+ `client_version` in the server's `drivers` table:
404
+
405
+ ```sql
406
+ SELECT client_name, client_version FROM drivers;
407
+ -- ruby | 1.0.3
408
+ ```
409
+
410
+ The server records the row asynchronously, so a `SELECT` immediately after
411
+ connecting may not show it yet. `Hello` is telemetry: a server that does not
412
+ know the opcode answers with an error the driver ignores.
413
+
414
+ ## Compatibility
415
+
416
+ - Ruby 2.7 – 3.4 (CI covers 3.1 – 3.4), MRI. No third-party dependencies;
417
+ `bigdecimal` is declared because it is a bundled gem since Ruby 3.4.
418
+ - Works with any skaidb server. Where a server predates an opcode the driver
419
+ falls back: prepared statements → client-side text binding, `Hello` →
420
+ ignored. `stream` and `exec_batch` need a server with those opcodes and
421
+ raise `QueryError` otherwise.
422
+ - The wire protocol (framing, SCRAM handshake, value encoding, opcodes) is
423
+ specified at <https://skaidb.org/docs/PROTOCOL.html>. The driver speaks the
424
+ binary protocol on port 7000; the server's REST/JSON gateway (port 7080)
425
+ is a dependency-free alternative for other clients.
426
+
427
+ ## Examples
428
+
429
+ [`examples/`](https://github.com/porcupin26/skaidb-ruby/tree/main/examples)
430
+ contains runnable scripts: `basic.rb`, `prepared_batch.rb`, `stream.rb`,
431
+ `pool.rb`, `tls.rb`, `subscribe.rb`. Each takes
432
+ `host port user password` on the command line and defaults to
433
+ `localhost:7000`; the arguments after those differ per script:
434
+
435
+ - `basic.rb`, `prepared_batch.rb`, `stream.rb`, `pool.rb`: `[database]`
436
+ - `subscribe.rb`: `[database] [stream]` (the stream defaults to
437
+ `orders_stream`)
438
+ - `tls.rb`: `[ca.crt]`, a CA file to trust; without it the script uses the
439
+ system trust store, or verifies nothing when `SKAIDB_TLS_INSECURE` is set
440
+ (development only). It takes no database argument.
441
+
442
+ ```sh
443
+ ruby examples/basic.rb localhost 7000 skaidb secret app
444
+ ruby examples/subscribe.rb localhost 7000 skaidb secret app orders_stream
445
+ ruby examples/tls.rb localhost 7000 skaidb secret /etc/skaidb/ca.crt
446
+ ```
447
+
448
+ ## Development
449
+
450
+ ```sh
451
+ git clone https://github.com/porcupin26/skaidb-ruby
452
+ cd skaidb-ruby
453
+ rake test # unit tests against an in-process fake server; no skaidb needed
454
+ gem build skaidb.gemspec
455
+ ```
456
+
457
+ The end-to-end test in `test/live/` runs against a real server when
458
+ `SKAIDB_LIVE=1` is set (with `SKAIDB_HOST`, `SKAIDB_PORT`, `SKAIDB_USER`,
459
+ `SKAIDB_PASSWORD`, `SKAIDB_DATABASE`), and is skipped otherwise:
460
+
461
+ ```sh
462
+ SKAIDB_LIVE=1 SKAIDB_HOST=127.0.0.1 SKAIDB_PORT=7000 SKAIDB_USER=admin SKAIDB_PASSWORD=secret \
463
+ SKAIDB_DATABASE=default rake test:live
464
+ ```
465
+
466
+ ### Releasing
467
+
468
+ A release is a tag. Bump `Skaidb::VERSION` in `lib/skaidb.rb`, add the
469
+ `## [x.y.z]` entry to `CHANGELOG.md`, commit, then tag and push:
470
+
471
+ ```sh
472
+ git tag v1.2.3 && git push origin main v1.2.3
473
+ ```
474
+
475
+ [`publish.yml`](https://github.com/porcupin26/skaidb-ruby/blob/main/.github/workflows/publish.yml)
476
+ then runs the tests, checks the tag against `Skaidb::VERSION`, builds the
477
+ gem, pushes it to [RubyGems.org](https://rubygems.org/gems/skaidb) with the
478
+ `RUBYGEMS_API_KEY` repository secret (a rubygems.org API key with the push
479
+ scope; without the secret the push is skipped with a notice) and creates the
480
+ GitHub Release with the `.gem` attached.
481
+
482
+ ## License
483
+
484
+ [SSPL-1.0](https://github.com/porcupin26/skaidb-ruby/blob/main/LICENSE) (Server Side Public License), the same license as skaidb.
data/docs/api.md ADDED
@@ -0,0 +1,146 @@
1
+ # API reference
2
+
3
+ Everything lives under the `Skaidb` module in `lib/skaidb.rb`.
4
+
5
+ ## `Skaidb.connect(**options) → Connection`
6
+
7
+ | Keyword | Default | Meaning |
8
+ |---|---|---|
9
+ | `host` | `"localhost"` | Node to dial (ignored when `seeds` is given). |
10
+ | `port` | `7000` | Binary-protocol port; also the port for a seed without one. |
11
+ | `user` | `"anonymous"` | SCRAM user name. |
12
+ | `password` | `""` | SCRAM password. Empty skips server-signature verification (anonymous connect). |
13
+ | `consistency` | `:quorum` | Default level for every statement: `:one`, `:quorum`, `:all`, `"ONE"`…, or `0`/`1`/`2`. |
14
+ | `timeout` | `10.0` | TCP connect timeout in seconds; `nil` for none. Reads are not bounded. |
15
+ | `database` | `nil` | Runs `USE "<database>"` after the handshake (and after every re-dial). |
16
+ | `seeds` | `nil` | `["host", "host:port", …]`, tried in random order until one connects **and** authenticates. |
17
+ | `tls` | `false` | Enable TLS with the system trust store. |
18
+ | `tls_ca` | `nil` | PEM file to trust instead of the system store; implies `tls`. |
19
+ | `tls_insecure` | `false` | TLS without any certificate verification; implies `tls`. Development only. |
20
+ | `tls_server_name` | `"skaidb"` | SNI name, also checked against the certificate's SANs. |
21
+
22
+ With a block, yields the connection and closes it when the block returns
23
+ (the block's value is returned). Raises `Skaidb::ConnectionError` when no
24
+ endpoint accepts the handshake (`no reachable endpoint in …: <last error>`),
25
+ `authentication denied: …` on a wrong password, and
26
+ `server signature mismatch` when the server fails mutual authentication.
27
+
28
+ ## `Skaidb::Connection`
29
+
30
+ ### Statements
31
+
32
+ - `exec(sql) → Result` (alias `query`) — run a statement with no parameters
33
+ at the connection's consistency.
34
+ - `exec_params(sql, params = [], consistency: nil) → Result` — bind `$1`,
35
+ `$2`, … from `params` (an Array), preparing the statement on the server and
36
+ sending the values typed. Falls back to client-side text binding when the
37
+ server refuses to prepare the statement (DDL, `USE`, old servers); on that
38
+ path an Array/Hash parameter raises `QueryError` that includes the server's
39
+ refusal reason. `consistency:` overrides the connection default for this
40
+ statement.
41
+ - `exec_batch(sql, rows, consistency: nil) → Integer` — prepare once, execute
42
+ once per row in one frame, return the total affected count. `rows` is an
43
+ Array of parameter Arrays. An empty `rows` returns `0` with no round-trip.
44
+ Raises `QueryError` if the statement cannot be prepared, if a row's length
45
+ differs from the statement's parameter count, or with the server's message
46
+ (which names the failing row index; earlier rows stay applied).
47
+ - `exec_prepared(id, params, consistency) → Result` — run a statement by the
48
+ id `prepare_server` returned, with an Array of typed values in `?` order.
49
+ - `prepare_server(sql) → [id, nparams] | nil` — prepare a statement with `?`
50
+ placeholders on the server; `nil` when the server refuses. Cached per
51
+ connection (240 entries); the cache is cleared on re-dial.
52
+ - `stream(sql, consistency: nil) { |row| … } → nil` — see
53
+ [streaming.md](streaming.md). Without a block returns an Enumerator.
54
+ - `subscribe(stream_name, after: nil, poll: 0.5) { |event| … }` — yields a
55
+ `CREATE STREAM` log's events forever, each a Hash with `"id"`, `"op"`,
56
+ `"k"`, `"ts"`, `"doc"`. Polls `_stream_<name>` 500 rows at a time with a
57
+ keyset cursor, sleeping `poll` seconds when caught up. `after:` is the last
58
+ `"id"` (an opaque String) already handled.
59
+
60
+ ### State
61
+
62
+ - `consistency` / `consistency=` — the default level (`0`/`1`/`2`; the setter
63
+ accepts anything `Consistency.resolve` does).
64
+ - `usable?` — `false` once closed, once a transport error broke the socket,
65
+ or while a stream is in flight (including one abandoned through external
66
+ iteration). No round-trip.
67
+ - `closed`, `finished?` — `true` after `close`.
68
+ - `close` — closes the socket; idempotent. Does not take the connection's
69
+ lock, so another thread can use it to break a blocked read.
70
+
71
+ ### Reconnect
72
+
73
+ A transport failure raises `Skaidb::ConnectionError` and marks the
74
+ connection broken. The next `exec`/`exec_params`/`exec_batch`/`stream`
75
+ re-dials across the original seeds, re-authenticates, re-sends `Hello`,
76
+ re-runs `USE`, and starts with an empty prepared-statement cache. If the
77
+ re-dial fails, that statement raises `ConnectionError` and the one after it
78
+ tries again.
79
+
80
+ ## `Skaidb::Result`
81
+
82
+ Shaped like `PG::Result`; `Enumerable` over row Hashes.
83
+
84
+ | Member | Meaning |
85
+ |---|---|
86
+ | `fields` (alias `columns`) | Column names, in order. |
87
+ | `nfields` / `num_fields` | Column count. |
88
+ | `rows` | Rows as Arrays of decoded values. |
89
+ | `ntuples` / `num_tuples` | Row count. |
90
+ | `each { |hash| }` / `to_a` / `values` | Rows as Hashes with String keys. |
91
+ | `[i]` | Row `i` as a Hash, or `nil`. |
92
+ | `getvalue(row, col)` | One cell; `col` is an index or a column name. |
93
+ | `cmd_tuples` | Rows affected by `INSERT`/`UPDATE`/`DELETE`; `0` for `SELECT` and DDL. |
94
+ | `result_sets` | For a `CALL` whose body `EMIT`s, every result set in order (each a `Result`); the last one is this result's own `rows`/`fields`. `[]` otherwise. |
95
+
96
+ ## `Skaidb::Pool`
97
+
98
+ `Skaidb::Pool.new(maxsize: 10, **connect_options)` — see
99
+ [pooling.md](pooling.md). `with { |conn| }`, `checkout`, `checkin(conn)`,
100
+ `close`. Raises `ArgumentError` for `maxsize < 1` and `Skaidb::Error` on
101
+ checkout from a closed pool.
102
+
103
+ ## `Skaidb::Uuid`
104
+
105
+ A value wrapper for binding a UUID with the Uuid type tag (results decode to
106
+ a canonical String). `Uuid.new(str)` accepts 32 hex digits with or without
107
+ dashes and raises `ArgumentError` otherwise; `Uuid.random` (version 4);
108
+ `Uuid.from_bytes(raw16)`; `to_s` (canonical lowercase), `bytes` (16 raw
109
+ bytes), `==`/`eql?`/`hash` (equal to another `Uuid` or to a String with the
110
+ same canonical form).
111
+
112
+ ## `Skaidb::Consistency`
113
+
114
+ `ONE = 0`, `QUORUM = 1`, `ALL = 2`; `resolve(value)` maps a Symbol, a String
115
+ (any case) or an Integer to the wire value and raises `Skaidb::Error`
116
+ otherwise.
117
+
118
+ ## Errors
119
+
120
+ - `Skaidb::Error < StandardError` — base class; raised directly for an
121
+ invalid consistency level and for checkout from a closed pool.
122
+ - `Skaidb::ConnectionError < Error` — dial, handshake, framing and transport
123
+ failures; a statement on a closed connection. The connection is broken and
124
+ will re-dial on the next statement.
125
+ - `Skaidb::QueryError < Error` — the server rejected the statement (the
126
+ message is the server's), or a parameter could not be bound
127
+ (placeholder/parameter mismatch, out-of-range Integer, non-finite Float or
128
+ BigDecimal, unsupported type). The connection stays usable.
129
+
130
+ ## Codec helpers
131
+
132
+ Exposed for tests and tooling: `Skaidb.encode_value(v)` / `Skaidb.decode_value(reader)`
133
+ (the §4 value codec), `Skaidb::Reader`, `Skaidb.quote(v)` and
134
+ `Skaidb.bind(sql, params)` (the client-side text fallback),
135
+ `Skaidb.to_qmark(sql, params)` (`$N` → `?` rewrite with the values in wire
136
+ order), `Skaidb.decimal_parts(bigdecimal)`, `Skaidb.time_ms(time)`,
137
+ `Skaidb.format_uuid(bytes)`, `Skaidb.scram(password, salt, iterations, auth_message)`.
138
+
139
+ ## `Skaidb::VERSION`
140
+
141
+ The package version, defined once in `lib/skaidb.rb`. The gemspec reads it
142
+ and the driver sends it in the `Hello` frame after every handshake; the
143
+ server shows it in the `drivers` table (`client_name = 'ruby'`,
144
+ `client_version = Skaidb::VERSION`). The server records that row
145
+ asynchronously, so a query issued immediately after connecting may not show
146
+ it yet.