tricoredb 0.1.0 → 0.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: 735d173a09ff6f3b5251bbade94d449f563aad10257266b7aaa6e15030ad2fe8
4
- data.tar.gz: 93d4dc22f7e1c09b0617d1826b5f7037a4c4313b58afd6c2493216dde1714161
3
+ metadata.gz: 35bb477aa16cb254d9745e4290a8ec5e052ba11e53dba60c8aa3b7a80aaff1a8
4
+ data.tar.gz: 3b6a18885b33a6625cb896a47856cf8def9c6c57b8c14cca85c282026ace41cb
5
5
  SHA512:
6
- metadata.gz: f55362c4d3caaec273f45bd09861a2ae5f3e6b47707914009828a20fc6be04d3a75e22fa5f6febe8857b4b106420dfe0ad7498b2fee00fbd92455ef8f6d4fda3
7
- data.tar.gz: 88df99ed1634d7c5eacd010861dbc72bf690d96500e282d2daa89d35a46249151e9d66eaa8003f2a75460e84f558dadff235b369cb9bd20a07bbafc043fc499f
6
+ metadata.gz: 5bb412d2aab2c127906911819e76d9d7703b1593a2d2cd01949be19f6c3534f1a5e440f907331ab60cec875aac4e955771018096d3d6a08b1faa3da71bcb856f
7
+ data.tar.gz: afc95e82a32c49fb6a5ca4dbeab10b8e42f93684842d1a42a8f3ee35b526746c3fba3654cb6152ea4715da9e9bcb20a221984ce6752601f2138c6f4f13c2f651
data/CHANGELOG.md CHANGED
@@ -6,6 +6,61 @@ All notable changes to the `tricoredb` gem are recorded here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.0] - 2026-10-13
10
+
11
+ Every method added here needs a server at **0.1.0-rc.3 or later**. An older server
12
+ does not know the operation and answers it with an error.
13
+
14
+ ### Added
15
+
16
+ - 116 methods, one for each operation the server gained since 0.1.0-rc.2. The README
17
+ lists every one beside the server operation it sends.
18
+ - Cache (53): sorted sets, locks with fencing tokens, bitmaps, HyperLogLog,
19
+ multi-key reads and writes, conditional and atomic read-and-change writes,
20
+ stale-while-revalidate reads, list moves and blocking pops, set algebra,
21
+ pipelines, transactions and scripts, publish and poll, namespace limits and
22
+ statistics.
23
+ - Documents (13): `doc_replace`, `doc_insert_many`, `doc_delete_many`,
24
+ `doc_bulk_write`, `doc_explain`, collection options, renaming, index rebuilds
25
+ and transactions spanning requests.
26
+ - Vectors (21): batch upserts and deletes, batch, multi-query and by-example
27
+ searches, named vectors, aliases, filters, counts, partitions, text embedding,
28
+ index statistics and collection configuration.
29
+ - Graphs (29): bulk writes, in-place updates, traversals and algorithms
30
+ (components, cycles, k shortest paths, centrality, communities, PageRank and
31
+ more), indexes and constraints, CSV import, Cypher writes and transactions.
32
+ - `TriCoreDB::BulkOp`, the four kinds of item a `doc_bulk_write` takes, and
33
+ `TriCoreDB::CacheOp`, the operations a cache pipeline, transaction or script runs.
34
+ - `TriCoreDB::ScoreBound` and `TriCoreDB::CacheExpiry`.
35
+ - `txn:` on `request` and on the document reads and writes, naming the document
36
+ transaction a call runs inside.
37
+
38
+ ### Fixed
39
+
40
+ - `in_transaction?` stays true while an outer transaction is open. Since 2026-09-29
41
+ the server opens a nested block when `BEGIN` is sent inside a transaction, and the
42
+ `COMMIT` or `ROLLBACK` that ends the block (answered with `nested_depth`) leaves
43
+ the outer transaction open. The driver used to report the transaction as over, so
44
+ a pool could lend the connection on with it still open.
45
+
46
+
47
+ ## [0.2.0] - 2026-09-17
48
+
49
+ ### Added
50
+
51
+ - Following the leader: a request refused with `not_leader` is sent again, to the
52
+ leader the refusal names (over a new connection that replaces the current one) or,
53
+ while no leader is known, to the same node after a pause. On by default;
54
+ `follow_leader: false` restores the previous behaviour, and `leader_retries` (5) and
55
+ `leader_retry_delay` (0.25 s) bound it. Never inside a session transaction.
56
+ - `TriCoreDB::OUTCOME_UNKNOWN`: a request that may or may not have been applied. It is
57
+ never sent again.
58
+ - `Client#address`.
59
+
60
+ ### Changed
61
+
62
+ - A `not_leader` error message says whether the client already retried.
63
+
9
64
  ## [0.1.0] - 2026-09-16
10
65
 
11
66
  First release.
@@ -42,5 +97,7 @@ First release.
42
97
  - A value with no SQL form is refused by name instead of being pushed through
43
98
  `to_s`, and text that is not valid UTF-8 is refused rather than sent as mojibake.
44
99
 
45
- [Unreleased]: https://github.com/trinesh14/tricoredb-sdk-ruby/compare/v0.1.0...HEAD
100
+ [Unreleased]: https://github.com/trinesh14/tricoredb-sdk-ruby/compare/v0.3.0...HEAD
101
+ [0.3.0]: https://github.com/trinesh14/tricoredb-sdk-ruby/compare/v0.2.0...v0.3.0
102
+ [0.2.0]: https://github.com/trinesh14/tricoredb-sdk-ruby/compare/v0.1.0...v0.2.0
46
103
  [0.1.0]: https://github.com/trinesh14/tricoredb-sdk-ruby/releases/tag/v0.1.0
data/README.md CHANGED
@@ -110,6 +110,9 @@ db.close
110
110
  | `tls` | `nil` | See [TLS](#tls) |
111
111
  | `client_name` | `"tricoredb-ruby/<version>"` | Name reported in the handshake |
112
112
  | `features` | every capability | Bitmap announced in the handshake |
113
+ | `follow_leader` | `true` | Send a `not_leader` refusal again, to the leader |
114
+ | `leader_retries` | `5` | How many times a refused request is sent again |
115
+ | `leader_retry_delay` | `0.25` | First pause, in seconds, before asking again; doubles up to 2 |
113
116
 
114
117
  A connection is a single request/response stream, so one connection serves one
115
118
  thread. For concurrent work use a [pool](#connection-pool).
@@ -290,6 +293,183 @@ path["node_path"] # => ["u1", "u2"]
290
293
  `graph_degree`, `graph_list_nodes`, `graph_list_edges` and `graph_query` for the
291
294
  read-only Cypher subset.
292
295
 
296
+ ## Operations added for server 0.1.0-rc.3
297
+
298
+ These need a server at **0.1.0-rc.3 or later**. An older server does not know the
299
+ operation and answers it with an error, which this client raises like any other
300
+ refusal.
301
+
302
+ ### Cache
303
+
304
+ | Method | Server operation | What it does |
305
+ | --- | --- | --- |
306
+ | `cache_set_fresh` | `Cache.SetFresh` | Store a value that answers as fresh for the given time and as stale after it, until its TTL (if any) ends |
307
+ | `cache_get_fresh` | `Cache.GetFresh` | Read with stale-while-revalidate and stampede protection |
308
+ | `cache_set_missing` | `Cache.SetMissing` | Negative caching |
309
+ | `cache_warm` | `Cache.Warm` | Cache warming |
310
+ | `cache_mget` | `Cache.MGet` | Read many values in one request |
311
+ | `cache_mset` | `Cache.MSet` | Write many values as one batch, all with the same optional TTL |
312
+ | `cache_set_if` | `Cache.SetIf` | Write a value under a condition, with explicit control over the TTL |
313
+ | `cache_zadd` | `Cache.ZAdd` | Add scored members to a sorted set, or re-score ones already there |
314
+ | `cache_zrem` | `Cache.ZRem` | Remove members from a sorted set, returning how many were present |
315
+ | `cache_zscore` | `Cache.ZScore` | The score of one member, or null when it is not in the set |
316
+ | `cache_zcard` | `Cache.ZCard` | How many members a sorted set holds |
317
+ | `cache_zrank` | `Cache.ZRank` | A member's position in the ordering, or null when it is absent |
318
+ | `cache_zrange` | `Cache.ZRange` | Members by rank, `start..=stop` inclusive |
319
+ | `cache_zrangebyscore` | `Cache.ZRangeByScore` | Members by score, between two bounds |
320
+ | `cache_zincrby` | `Cache.ZIncrBy` | Add to a member's score, creating it at that score when absent |
321
+ | `cache_lock_acquire` | `Cache.LockAcquire` | Take a lease on a lock, or refresh one already held by `owner` |
322
+ | `cache_lock_renew` | `Cache.LockRenew` | Extend a lease the caller still holds |
323
+ | `cache_lock_release` | `Cache.LockRelease` | Give up a lock |
324
+ | `cache_setbit` | `Cache.SetBit` | Set one bit of a string value, growing it with zero bytes if needed |
325
+ | `cache_getbit` | `Cache.GetBit` | Read one bit |
326
+ | `cache_bitcount` | `Cache.BitCount` | Count the set bits, optionally within a byte range `(start, end)` with signed indices |
327
+ | `cache_bitpos` | `Cache.BitPos` | The position of the first bit equal to `bit`, optionally within a byte range, or -1 when there is none |
328
+ | `cache_bitop` | `Cache.BitOp` | Combine several values bitwise and store the result at `dest` |
329
+ | `cache_pfadd` | `Cache.PfAdd` | Add elements to a HyperLogLog, creating it when absent |
330
+ | `cache_pfcount` | `Cache.PfCount` | Estimated number of distinct elements |
331
+ | `cache_pfmerge` | `Cache.PfMerge` | Merge several HyperLogLogs into `dest`, which is included as a source when it already exists |
332
+ | `cache_lmove` | `Cache.LMove` | Atomically pop an element from one end of `source` and push it onto one end of `destination` |
333
+ | `cache_expire_at` | `Cache.ExpireAt` | Set the absolute instant an existing live key dies |
334
+ | `cache_stats` | `Cache.Stats` | What this node's cache is doing |
335
+ | `cache_flush_database` | `Cache.FlushDatabase` | Delete every cache key in this database, across every namespace |
336
+ | `cache_append` | `Cache.Append` | Concatenate bytes onto what a key already holds, and return the new length |
337
+ | `cache_compare_and_swap` | `Cache.CompareAndSwap` | Write only if the key still holds `expected` (null |
338
+ | `cache_getdel` | `Cache.GetDel` | Read a key and delete it in one atomic step |
339
+ | `cache_getset` | `Cache.GetSet` | Write a value and return the one it replaced, atomically |
340
+ | `cache_getex` | `Cache.GetEx` | Read a key and change its expiry in the same atomic step |
341
+ | `cache_ltrim` | `Cache.LTrim` | Keep only the inclusive index range of a list, dropping everything outside it |
342
+ | `cache_sunion` | `Cache.SUnion` | Every member of every named set, without duplicates |
343
+ | `cache_sinter` | `Cache.SInter` | The members present in every named set |
344
+ | `cache_sdiff` | `Cache.SDiff` | The members of the first set that are in none of the rest |
345
+ | `cache_srandmember` | `Cache.SRandMember` | Random members of a set, without removing them |
346
+ | `cache_hincrby` | `Cache.HIncrBy` | Add to one hash field, atomically, returning its new value |
347
+ | `cache_pipeline` | `Cache.Pipeline` | Run cache operations in order in one round trip, each exactly as if it had been sent alone |
348
+ | `cache_transaction` | `Cache.Transaction` | Run cache operations in order holding the cache write lock, so no other writer interleaves with them |
349
+ | `cache_script_load` | `Cache.ScriptLoad` | Store a script |
350
+ | `cache_script_run` | `Cache.ScriptRun` | Run a stored script with these arguments as one transaction |
351
+ | `cache_hot_keys` | `Cache.HotKeys` | The most frequently addressed keys on this node since it started, with estimated counts |
352
+ | `cache_purge_expired` | `Cache.PurgeExpired` | Delete every expired cache key in this database now, rather than leaving it to compaction |
353
+ | `cache_blpop` | `Cache.BLPop` | Pop the head of the first non-empty list among `keys`, waiting up to the timeout for one to receive an element |
354
+ | `cache_brpop` | `Cache.BRPop` | Pop the tail of the first non-empty list among `keys`, waiting up to the timeout for one to receive an element |
355
+ | `cache_set_namespace_limit` | `Cache.SetNamespaceLimit` | Cap a namespace at `max_keys` live keys and `max_bytes` of values |
356
+ | `cache_get_namespace_limit` | `Cache.GetNamespaceLimit` | A namespace's limit and what it holds now |
357
+ | `cache_publish` | `Cache.Publish` | Append a message to a channel, returning the id it was given |
358
+ | `cache_poll` | `Cache.Poll` | The messages after id `after` on these channels (a trailing `*` is a pattern) |
359
+
360
+ ### Documents
361
+
362
+ | Method | Server operation | What it does |
363
+ | --- | --- | --- |
364
+ | `doc_replace` | `Document.Replace` | Swap a document's whole body for a new one |
365
+ | `doc_insert_many` | `Document.InsertMany` | Insert several documents as one atomic batch |
366
+ | `doc_delete_many` | `Document.DeleteMany` | Delete every document a filter matches, returning how many went |
367
+ | `doc_rename_collection` | `Document.RenameCollection` | Move a collection, with its documents and indexes, to a new name |
368
+ | `doc_purge_expired` | `Document.PurgeExpired` | Delete the documents a TTL index says have expired, now |
369
+ | `doc_explain` | `Document.Explain` | Run a find and describe how it ran instead of returning its documents |
370
+ | `doc_begin_transaction` | `Document.BeginTransaction` | Open a document transaction spanning requests, returning its id |
371
+ | `doc_commit_transaction` | `Document.CommitTransaction` | Commit a document transaction |
372
+ | `doc_rollback_transaction` | `Document.RollbackTransaction` | Discard a document transaction |
373
+ | `doc_rebuild_index` | `Document.RebuildIndex` | Rebuild one index from the stored documents |
374
+ | `doc_set_collection_options` | `Document.SetCollectionOptions` | Set a collection's options (`schema`, `defaults`, `versioned`), replacing any it had |
375
+ | `doc_get_collection_options` | `Document.GetCollectionOptions` | A collection's options |
376
+ | `doc_bulk_write` | `Document.BulkWrite` | Many inserts, updates, replaces and deletes on one collection, applied in order as one atomic batch |
377
+
378
+ ### Vectors
379
+
380
+ | Method | Server operation | What it does |
381
+ | --- | --- | --- |
382
+ | `vector_embed` | `Vector.Embed` | Embed texts with the server's built-in embedder (deterministic feature hashing |
383
+ | `vector_upsert_text` | `Vector.UpsertText` | Embed each text at the collection's dimension and upsert the vectors as one atomic batch |
384
+ | `vector_set_alias` | `Vector.SetAlias` | Point an alias at a collection; a null collection removes it |
385
+ | `vector_list_aliases` | `Vector.ListAliases` | Every alias and the collection it names, as `{alias, collection}` |
386
+ | `vector_add_named_vector` | `Vector.AddNamedVector` | Declare a named vector on a collection |
387
+ | `vector_upsert_named` | `Vector.UpsertNamed` | Write an item's vectors by name (`default` is the collection's own), each with the same metadata |
388
+ | `vector_upsert_many` | `Vector.UpsertMany` | Write many vectors as one atomic batch |
389
+ | `vector_delete_many` | `Vector.DeleteMany` | Delete many vectors by id as one atomic batch, returning how many of the ids were present |
390
+ | `vector_search_batch` | `Vector.SearchBatch` | Run several query vectors against one collection in one request |
391
+ | `vector_delete_by_filter` | `Vector.DeleteByFilter` | Delete every vector whose metadata matches a filter, as one atomic batch |
392
+ | `vector_update_by_filter` | `Vector.UpdateByFilter` | Merge `set` into the metadata of every vector matching a filter, as one atomic batch |
393
+ | `vector_count` | `Vector.Count` | How many vectors a collection holds, or, with a filter, how many match it |
394
+ | `vector_list_partitions` | `Vector.ListPartitions` | The partitions of a collection |
395
+ | `vector_rename_collection` | `Vector.RenameCollection` | Rename a collection, moving its catalog record and every vector with it |
396
+ | `vector_rebuild_index` | `Vector.RebuildIndex` | Rebuild the collection's ANN index from its stored vectors and persist it, clearing every tombstone |
397
+ | `vector_index_stats` | `Vector.IndexStats` | The shape of the collection's ANN index |
398
+ | `vector_measure_recall` | `Vector.MeasureRecall` | Measure the ANN index's recall |
399
+ | `vector_search_multi` | `Vector.SearchMulti` | Search with several query vectors and return one fused ranking |
400
+ | `vector_set_collection_config` | `Vector.SetCollectionConfig` | Change a collection's configuration, returning it |
401
+ | `vector_copy_collection` | `Vector.CopyCollection` | Copy a collection (configuration and every vector) to a new name |
402
+ | `vector_search_by_examples` | `Vector.SearchByExamples` | Search with a query built from examples |
403
+
404
+ ### Graphs
405
+
406
+ | Method | Server operation | What it does |
407
+ | --- | --- | --- |
408
+ | `graph_add_many` | `Graph.AddMany` | Write many nodes and edges as one unit |
409
+ | `graph_remove_many` | `Graph.RemoveMany` | Delete many nodes and edges as one unit |
410
+ | `graph_update_node` | `Graph.UpdateNode` | Change an existing node in place, returning it |
411
+ | `graph_update_edge` | `Graph.UpdateEdge` | Change an existing edge in place, returning it |
412
+ | `graph_copy` | `Graph.CopyGraph` | Copy a graph (nodes, edges and schema) to a new name |
413
+ | `graph_dfs` | `Graph.Dfs` | A bounded depth-first walk from a node |
414
+ | `graph_connected_components` | `Graph.ConnectedComponents` | Partition the graph's nodes into connected components, largest first |
415
+ | `graph_detect_cycle` | `Graph.DetectCycle` | Whether the graph contains a cycle (`hasCycle`), and one `cycle` as evidence |
416
+ | `graph_stats` | `Graph.GraphStats` | Counts describing a graph |
417
+ | `graph_subgraph` | `Graph.Subgraph` | The part of the graph within `max_depth` hops of the seeds |
418
+ | `graph_schema` | `Graph.GraphSchema` | What the graph's data looks like |
419
+ | `graph_k_shortest_paths` | `Graph.KShortestPaths` | The `k` cheapest loopless paths between two nodes, cheapest first (default 3, at most 20) |
420
+ | `graph_all_paths` | `Graph.AllPaths` | Every simple path between two nodes of at most `max_depth` edges (default 5, at most 10) |
421
+ | `graph_centrality` | `Graph.Centrality` | Betweenness, closeness or eigenvector centrality |
422
+ | `graph_communities` | `Graph.Communities` | Communities by Louvain modularity optimisation, direction ignored, largest first, with the partition's `modularity` |
423
+ | `graph_triangle_count` | `Graph.TriangleCount` | Triangles and clustering coefficients, direction ignored |
424
+ | `graph_node_similarity` | `Graph.NodeSimilarity` | The nodes whose neighbour sets most resemble a node's, under `results` |
425
+ | `graph_check_consistency` | `Graph.CheckConsistency` | Check a graph's stored records against each other and against the resident mirror |
426
+ | `graph_create_node_index` | `Graph.CreateNodeIndex` | Index node properties for finding nodes |
427
+ | `graph_create_constraint` | `Graph.CreateConstraint` | Enforce a rule on a property of every node (or, with target `edge`, every relationship) carrying a label |
428
+ | `graph_drop_schema_object` | `Graph.DropSchemaObject` | Drop an index or a constraint by name |
429
+ | `graph_list_schema_objects` | `Graph.ListSchemaObjects` | A graph's `indexes` and `constraints` |
430
+ | `graph_find_nodes` | `Graph.FindNodes` | Nodes with a label (when given) whose properties equal the given values, in id order |
431
+ | `graph_import_csv` | `Graph.ImportCsv` | Import nodes and edges from CSV text as one atomic batch, so constraints apply |
432
+ | `graph_page_rank` | `Graph.PageRank` | PageRank over edge direction |
433
+ | `graph_execute` | `Graph.Execute` | Run a Cypher write |
434
+ | `graph_begin_transaction` | `Graph.BeginTransaction` | Open a transaction on a graph, returning its id |
435
+ | `graph_commit_transaction` | `Graph.CommitTransaction` | Apply every statement queued in a graph transaction as one |
436
+ | `graph_rollback_transaction` | `Graph.RollbackTransaction` | Discard a graph transaction and its queued statements |
437
+ `doc_bulk_write` takes its operations from `TriCoreDB::BulkOp` (`insert`,
438
+ `update_one`, `replace`, `delete`), and a cache pipeline, transaction or script takes
439
+ its from `TriCoreDB::CacheOp`:
440
+
441
+ ```ruby
442
+ db.doc_bulk_write("people", [
443
+ TriCoreDB::BulkOp.insert({ "name" => "ada" }, id: "a"),
444
+ TriCoreDB::BulkOp.update_one("a", set: { "city" => "Pune" }),
445
+ TriCoreDB::BulkOp.delete("b")
446
+ ]) # every operation lands, or none does
447
+
448
+ results = db.cache_pipeline([
449
+ TriCoreDB::CacheOp.set("sessions", "abc", "ada"),
450
+ TriCoreDB::CacheOp.get("sessions", "abc")
451
+ ]) # => [{"ok" => {"Json" => {"set" => true}}}, {"ok" => {"CacheValue" => [97, 100, 97]}}]
452
+ ```
453
+
454
+ A document transaction is named in each call that runs inside it, and a graph
455
+ transaction queues Cypher writes until it commits:
456
+
457
+ ```ruby
458
+ txn = db.doc_begin_transaction
459
+ db.doc_insert("people", { "name" => "ada" }, id: "a", txn: txn)
460
+ db.doc_commit_transaction(txn)
461
+
462
+ graph_txn = db.graph_begin_transaction("social")
463
+ db.graph_execute("social", "CREATE (n:Person {name: 'ada'})", txn: graph_txn)
464
+ db.graph_commit_transaction("social", graph_txn)
465
+ ```
466
+
467
+ A result with one meaningful field comes back as that value; a larger one comes back
468
+ as the server's own Hash, with the server's field names. Sorted-set bounds come from
469
+ `TriCoreDB::ScoreBound` (a bare number is an inclusive bound) and expiries from
470
+ `TriCoreDB::CacheExpiry`; search `options:` are the server's own names (`offset`,
471
+ `exact`, `using`, `namespace`, ...).
472
+
293
473
  ## LLM context
294
474
 
295
475
  ```ruby
@@ -329,11 +509,24 @@ Every failure is a `TriCoreDB::Error`. Rescue the class you mean, and branch on
329
509
  | `ParameterError` | A value with no SQL form; nothing was sent. |
330
510
  | `PoolTimeout` | No pooled connection became free in time. |
331
511
 
332
- **Leader redirects.** In a cluster, a write that reaches a follower fails with
333
- `code == "not_leader"`, which `error.redirect?` and `response.redirect?` test. When
334
- the cluster knows the leader, `leader_hint` holds its `host:port`; a `nil` hint means
335
- the destination is unknown yet, so wait and retry — it does not mean the failure was
336
- something else. This driver does not follow the redirect for you.
512
+ **Leader redirects.** In a cluster only the leader accepts writes, and a follower
513
+ refuses with `code == "not_leader"` (`error.redirect?`). The client handles that
514
+ itself (`follow_leader: true`, the default):
515
+
516
+ - when the refusal names the leader (`leader_hint`), it opens a connection there with
517
+ the same user, secret and TLS settings (the leader's host is the certificate name),
518
+ sends the request again, and keeps that connection (`db.address` reports it);
519
+ - while an election is in progress (no hint), it waits and asks the same node again,
520
+ pausing 0.25 s, then 0.5 s, up to 2 s;
521
+ - after `leader_retries` (5) it raises the last refusal.
522
+
523
+ This is safe because a `not_leader` refusal was applied nowhere. Nothing is sent again
524
+ inside a session transaction, and never a refusal with `TriCoreDB::OUTCOME_UNKNOWN`: a
525
+ follower handed that request to the leader and lost the answer, or the leader could
526
+ not confirm it, so it may have been applied. The leader's address must be reachable
527
+ from your application; when it is not, the client keeps its connection and waits. A
528
+ pool passes these options to every connection. To handle redirects yourself, connect
529
+ with `follow_leader: false`:
337
530
 
338
531
  ```ruby
339
532
  begin
@@ -370,7 +563,9 @@ rake test:integration # the live tests, against a private tricore-server
370
563
  The scripted-peer tests play the answers a real cluster would send — a `not_leader`
371
564
  refusal, a refused handshake, a frame that declares more bytes than it sends — so
372
565
  they need no server. The live tests start their own `tricore-server` on an ephemeral
373
- port: point `TRICORE_SERVER_BIN` at the binary, and without one they skip.
566
+ port: point `TRICORE_SERVER_BIN` at the binary. To use a server that is already
567
+ running instead, set `TRICORE_HOST`, `TRICORE_PORT`, `TRICORE_USER` and
568
+ `TRICORE_SECRET`. Without either they skip.
374
569
 
375
570
  ## License
376
571
 
@@ -110,6 +110,148 @@ module TriCoreDB
110
110
  end
111
111
  end
112
112
 
113
+ # What a cache call does to a key's expiry.
114
+ module CacheExpiry
115
+ module_function
116
+
117
+ # Leave the expiry as it is.
118
+ def keep
119
+ "Keep"
120
+ end
121
+
122
+ # Remove the expiry, which is what a plain set does.
123
+ def clear
124
+ "Clear"
125
+ end
126
+
127
+ # Expire `ttl_ms` milliseconds from now.
128
+ # @return [Hash]
129
+ def set(ttl_ms)
130
+ { "Set" => { "ttl_ms" => Integer(ttl_ms) } }
131
+ end
132
+ end
133
+
134
+ # One end of a sorted-set score range.
135
+ #
136
+ # JSON has no infinity, so an unbounded end is sent as the largest finite Float,
137
+ # which no finite score lies beyond.
138
+ module ScoreBound
139
+ module_function
140
+
141
+ # A bound that includes members scoring exactly `value`.
142
+ # @return [Hash]
143
+ def inclusive(value)
144
+ bound(value, false)
145
+ end
146
+
147
+ # A bound that leaves out members scoring exactly `value`.
148
+ # @return [Hash]
149
+ def exclusive(value)
150
+ bound(value, true)
151
+ end
152
+
153
+ # No lower limit.
154
+ def lowest
155
+ bound(-Float::INFINITY, false)
156
+ end
157
+
158
+ # No upper limit.
159
+ def highest
160
+ bound(Float::INFINITY, false)
161
+ end
162
+
163
+ # @api private
164
+ def bound(value, exclusive)
165
+ number = Float(value)
166
+ raise ArgumentError, "a score bound must not be NaN" if number.nan?
167
+
168
+ number = number.positive? ? Float::MAX : -Float::MAX if number.infinite?
169
+ { "value" => number, "exclusive" => exclusive }
170
+ end
171
+ end
172
+
173
+ # Builders for the cache operations a pipeline, a transaction or a script runs.
174
+ # Each is the operation exactly as it would be sent alone.
175
+ #
176
+ # @example
177
+ # db.cache_pipeline([TriCoreDB::CacheOp.set("sessions", "abc", "ada"), TriCoreDB::CacheOp.get("sessions", "abc")])
178
+ module CacheOp
179
+ module_function
180
+
181
+ # A liveness check.
182
+ def ping
183
+ "Ping"
184
+ end
185
+
186
+ # Any cache operation by its server name, with its fields as the server names
187
+ # them. Byte fields are Arrays of byte values.
188
+ # @return [Hash]
189
+ def raw(variant, body)
190
+ { variant.to_s => body }
191
+ end
192
+
193
+ # @param value [String, Array<Integer>]
194
+ def set(namespace, key, value, ttl_ms: nil)
195
+ raw("Set", "namespace" => namespace.to_s, "key" => key.to_s,
196
+ "value" => value.is_a?(String) ? value.bytes : value, "ttl_ms" => ttl_ms)
197
+ end
198
+
199
+ def get(namespace, key)
200
+ raw("Get", "namespace" => namespace.to_s, "key" => key.to_s)
201
+ end
202
+
203
+ def delete(namespace, key)
204
+ raw("Delete", "namespace" => namespace.to_s, "key" => key.to_s)
205
+ end
206
+
207
+ def exists(namespace, key)
208
+ raw("Exists", "namespace" => namespace.to_s, "key" => key.to_s)
209
+ end
210
+
211
+ def incr(namespace, key, by = 1)
212
+ raw("Incr", "namespace" => namespace.to_s, "key" => key.to_s, "by" => Integer(by))
213
+ end
214
+
215
+ def expire(namespace, key, ttl_ms)
216
+ raw("Expire", "namespace" => namespace.to_s, "key" => key.to_s, "ttl_ms" => Integer(ttl_ms))
217
+ end
218
+ end
219
+
220
+ # Builders for the operations of {Client#doc_bulk_write}.
221
+ module BulkOp
222
+ module_function
223
+
224
+ # Insert a document, under `id` when one is given.
225
+ # @return [Hash]
226
+ def insert(document, id: nil)
227
+ { "Insert" => { "id" => id, "document" => document } }
228
+ end
229
+
230
+ # Set or increment fields on one document. `expected_version` applies it only
231
+ # if the document's `_version` is still that.
232
+ # @return [Hash]
233
+ def update_one(id, set: nil, inc: nil, upsert: false, expected_version: nil)
234
+ update = {}
235
+ update["set"] = set if set && !set.empty?
236
+ update["inc"] = inc if inc && !inc.empty?
237
+ { "UpdateOne" => { "id" => id, "update" => update, "upsert" => upsert ? true : false,
238
+ "expected_version" => expected_version } }
239
+ end
240
+
241
+ # Swap a document's whole body for `document`.
242
+ # @return [Hash]
243
+ def replace(id, document, upsert: false, expected_version: nil)
244
+ { "Replace" => { "id" => id, "document" => document, "upsert" => upsert ? true : false,
245
+ "expected_version" => expected_version } }
246
+ end
247
+
248
+ # Delete one document by id.
249
+ # @return [Hash]
250
+ def delete(id, expected_version: nil)
251
+ { "Delete" => { "id" => id, "expected_version" => expected_version } }
252
+ end
253
+ end
254
+
113
255
  # Builders for `LlmSource`.
114
256
  module LlmSource
115
257
  module_function