Class: TriCoreDB::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/tricoredb/client.rb

Overview

One connection to a TriCoreDB server, speaking the native tricore protocol.

A connection is a single request/response stream. Calls from several threads are serialised by an internal lock, so they are safe but not concurrent; use a Pool for concurrency.

Examples:

db = TriCoreDB.connect(host: "127.0.0.1", port: 8427, user: "admin", secret: ENV["TRICORE_SECRET"])
db.execute("INSERT INTO t VALUES (?, ?)", [1, "ada"])
db.query("SELECT name FROM t WHERE id = ?", [1]).rows # => [["ada"]]
db.close

Constant Summary collapse

DEFAULT_PORT =
8427
DEFAULT_DATABASE =
"main"
CLOSE_TIMEOUT =
2

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(transport, database: DEFAULT_DATABASE) ⇒ Client

Wrap an already-open transport. connect is the usual entry point.

Parameters:

  • transport (Transport)
  • database (String) (defaults to: DEFAULT_DATABASE)


83
84
85
86
87
88
89
90
91
92
93
# File 'lib/tricoredb/client.rb', line 83

def initialize(transport, database: DEFAULT_DATABASE)
  @transport = transport
  @database = database
  @lock = Mutex.new
  @rid = 0
  @rid_prefix = "rb-#{Process.pid.to_s(16)}-#{SecureRandom.hex(6)}"
  @granted_features = 0
  @txn_open = false
  @read_timeout = nil
  @request_timeout_ms = nil
end

Instance Attribute Details

#database ⇒ String

Returns the database used when a call does not name one.

Returns:

  • (String) —

    the database used when a call does not name one



41
42
43
# File 'lib/tricoredb/client.rb', line 41

def database
  @database
end

#granted_features ⇒ Integer (readonly)

Returns the feature bitmap the server granted in HELLO_OK.

Returns:

  • (Integer) —

    the feature bitmap the server granted in HELLO_OK



35
36
37
# File 'lib/tricoredb/client.rb', line 35

def granted_features
  @granted_features
end

#last_request_id ⇒ String? (readonly)

Returns the request id most recently sent; pass it to #cancel on another connection.

Returns:

  • (String, nil) —

    the request id most recently sent; pass it to #cancel on another connection



39
40
41
# File 'lib/tricoredb/client.rb', line 39

def last_request_id
  @last_request_id
end

#read_timeout ⇒ Numeric?

Returns client-side seconds to wait for a reply; nil (the default) waits for as long as the statement runs.

Returns:

  • (Numeric, nil) —

    client-side seconds to wait for a reply; nil (the default) waits for as long as the statement runs



43
44
45
# File 'lib/tricoredb/client.rb', line 43

def read_timeout
  @read_timeout
end

#request_timeout_ms ⇒ Integer?

Returns a server-side deadline in milliseconds sent with every request.

Returns:

  • (Integer, nil) —

    a server-side deadline in milliseconds sent with every request



45
46
47
# File 'lib/tricoredb/client.rb', line 45

def request_timeout_ms
  @request_timeout_ms
end

#session_id ⇒ String? (readonly)

Returns the session id issued by AUTH_OK.

Returns:

  • (String, nil) —

    the session id issued by AUTH_OK



37
38
39
# File 'lib/tricoredb/client.rb', line 37

def session_id
  @session_id
end

Class Method Details

.connect(host: "127.0.0.1", port: DEFAULT_PORT, user: nil, secret: "", database: DEFAULT_DATABASE, connect_timeout: 10, read_timeout: nil, request_timeout_ms: nil, tls: nil, client_name: "tricoredb-ruby/#{VERSION}", features: Features::ALL) ⇒ Client

Connect, handshake and (when user is given) authenticate.

Parameters:

  • host (String) (defaults to: "127.0.0.1")
  • port (Integer) (defaults to: DEFAULT_PORT)
  • user (String, nil) (defaults to: nil)
  • secret (String) (defaults to: "") —

    password or token; sent as its bytes

  • database (String) (defaults to: DEFAULT_DATABASE)
  • connect_timeout (Numeric, nil) (defaults to: 10) —

    seconds covering TCP connect, TLS, HELLO and AUTH

  • read_timeout (Numeric, nil) (defaults to: nil) —

    seconds to wait for each reply after connecting

  • request_timeout_ms (Integer, nil) (defaults to: nil) —

    server-side deadline for every request

  • tls (Hash, true, nil) (defaults to: nil) —
  • client_name (String) (defaults to: "tricoredb-ruby/#{VERSION}")
  • features (Integer) (defaults to: Features::ALL) —

    capabilities to request

Returns:

Raises:



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/tricoredb/client.rb', line 62

def self.connect(host: "127.0.0.1", port: DEFAULT_PORT, user: nil, secret: "", database: DEFAULT_DATABASE,
                 connect_timeout: 10, read_timeout: nil, request_timeout_ms: nil, tls: nil,
                 client_name: "tricoredb-ruby/#{VERSION}", features: Features::ALL)
  transport = Transport.open(host, Integer(port), connect_timeout: connect_timeout, tls: tls)
  client = new(transport, database: database)
  begin
    client.handshake(client_name: client_name, features: features, timeout: connect_timeout)
    client.authenticate(user, secret, timeout: connect_timeout) unless user.nil?
  rescue Exception # rubocop:disable Lint/RescueException
    transport.close
    raise
  end
  client.read_timeout = read_timeout
  client.request_timeout_ms = request_timeout_ms
  client
end

Instance Method Details

#admin_ping(database: @database) ⇒ Object

A request through the full pipeline (auth, routing, dispatch).



772
773
774
775
# File 'lib/tricoredb/client.rb', line 772

def admin_ping(database: @database)
  request({ "Admin" => "Ping" }, database: database)
  true
end

#admin_status(database: @database) ⇒ Hash

Returns:

  • (Hash)


778
779
780
781
782
783
# File 'lib/tricoredb/client.rb', line 778

def admin_status(database: @database)
  resp = request({ "Admin" => "Status" }, database: database)
  return { "message" => resp.arm("Message") } if resp.arm?("Message")

  json(resp, "status")
end

#authenticate(user, secret, timeout: @read_timeout) ⇒ String?

Send AUTH and read AUTH_OK.

A refused login arrives as AUTH_OK with ok: false, not as an ERROR frame; both are raised as AuthError.

Returns:

  • (String, nil) —

    the session id

Raises:



143
144
145
146
147
148
149
150
151
152
# File 'lib/tricoredb/client.rb', line 143

def authenticate(user, secret, timeout: @read_timeout)
  tag, body = exchange(Frame::AUTH, { "username" => user.to_s, "secret" => secret.to_s.bytes }, timeout: timeout)
  raise AuthError.new(error_text(body), code: error_code(body)) if tag == Frame::ERROR
  fail_stream(ProtocolError.new("expected AUTH_OK, got #{Frame.name(tag)}")) unless tag == Frame::AUTH_OK
  unless body.is_a?(Hash) && body["ok"] == true
    raise AuthError, (body.is_a?(Hash) && body["message"]) || "authentication refused"
  end

  @session_id = body["session_id"]
end

#begin_transaction(database: @database) ⇒ Hash Also known as: begin

Open a session transaction on this connection. Requires SESSION_TXN.

Returns:

  • (Hash) —

    the server's outcome

Raises:



257
258
259
260
261
# File 'lib/tricoredb/client.rb', line 257

def begin_transaction(database: @database)
  require_feature(Features::SESSION_TXN, "SESSION_TXN",
                  "begin/commit/rollback (send a whole `BEGIN; ...; COMMIT` script with #execute instead)")
  txn_control("BEGIN", database)
end

#cache_clear_namespace(namespace, database: @database) ⇒ Integer

Returns keys removed.

Returns:

  • (Integer) —

    keys removed



339
340
341
# File 'lib/tricoredb/client.rb', line 339

def cache_clear_namespace(namespace, database: @database)
  json(cache("ClearNamespace", { "namespace" => namespace }, database), "clear")["cleared"]
end

#cache_delete(namespace, key, database: @database) ⇒ Boolean

Returns whether the key existed.

Returns:

  • (Boolean) —

    whether the key existed



323
324
325
# File 'lib/tricoredb/client.rb', line 323

def cache_delete(namespace, key, database: @database)
  json(cache("Delete", nk(namespace, key), database), "delete")["deleted"] == true
end

#cache_exists?(namespace, key, database: @database) ⇒ Boolean

Returns:

  • (Boolean)


328
329
330
# File 'lib/tricoredb/client.rb', line 328

def cache_exists?(namespace, key, database: @database)
  json(cache("Exists", nk(namespace, key), database), "exists")["exists"] == true
end

#cache_expire(namespace, key, ttl_ms, database: @database) ⇒ Boolean

Returns false when the key does not exist.

Returns:

  • (Boolean) —

    false when the key does not exist



349
350
351
# File 'lib/tricoredb/client.rb', line 349

def cache_expire(namespace, key, ttl_ms, database: @database)
  json(cache("Expire", nk(namespace, key).merge("ttl_ms" => Integer(ttl_ms)), database), "expire")["updated"] == true
end

#cache_get(namespace, key, database: @database) ⇒ String?

Returns binary String, nil on a miss.

Returns:

  • (String, nil) —

    binary String, nil on a miss



318
319
320
# File 'lib/tricoredb/client.rb', line 318

def cache_get(namespace, key, database: @database)
  cache_value(cache("Get", nk(namespace, key), database), "get")
end

#cache_hdel(namespace, key, fields, database: @database) ⇒ Integer

Returns fields that were present.

Returns:

  • (Integer) —

    fields that were present



446
447
448
# File 'lib/tricoredb/client.rb', line 446

def cache_hdel(namespace, key, fields, database: @database)
  json(cache("HDel", nk(namespace, key).merge("fields" => byte_list(fields, "fields")), database), "hdel")["deleted"]
end

#cache_hexists?(namespace, key, field, database: @database) ⇒ Boolean

Returns:

  • (Boolean)


458
459
460
# File 'lib/tricoredb/client.rb', line 458

def cache_hexists?(namespace, key, field, database: @database)
  json(cache("HExists", nk(namespace, key).merge("field" => bytes(field, "field")), database), "hexists")["exists"] == true
end

#cache_hget(namespace, key, field, database: @database) ⇒ String?

Returns:

  • (String, nil)


441
442
443
# File 'lib/tricoredb/client.rb', line 441

def cache_hget(namespace, key, field, database: @database)
  cache_value(cache("HGet", nk(namespace, key).merge("field" => bytes(field, "field")), database), "hget")
end

#cache_hgetall(namespace, key, database: @database) ⇒ Array<Array(String, String)>

Returns binary pairs in ascending field order.

Returns:

  • (Array<Array(String, String)>) —

    binary pairs in ascending field order



451
452
453
454
455
# File 'lib/tricoredb/client.rb', line 451

def cache_hgetall(namespace, key, database: @database)
  (json(cache("HGetAll", nk(namespace, key), database), "hgetall")["entries"] || []).map do |f, v|
    [f.pack("C*"), v.pack("C*")]
  end
end

#cache_hlen(namespace, key, database: @database) ⇒ Integer

Returns:

  • (Integer)


463
464
465
# File 'lib/tricoredb/client.rb', line 463

def cache_hlen(namespace, key, database: @database)
  json(cache("HLen", nk(namespace, key), database), "hlen")["length"]
end

#cache_hset(namespace, key, entries, database: @database) ⇒ Integer

Returns fields newly created.

Parameters:

  • entries (Hash, Array<Array(String, String)>) —

    field => value

Returns:

  • (Integer) —

    fields newly created



436
437
438
# File 'lib/tricoredb/client.rb', line 436

def cache_hset(namespace, key, entries, database: @database)
  json(cache("HSet", nk(namespace, key).merge("entries" => pairs(entries, "entries")), database), "hset")["created"]
end

#cache_incr(namespace, key, by = 1, database: @database) ⇒ Integer

Returns the new value.

Returns:

  • (Integer) —

    the new value



344
345
346
# File 'lib/tricoredb/client.rb', line 344

def cache_incr(namespace, key, by = 1, database: @database)
  json(cache("Incr", nk(namespace, key).merge("by" => Integer(by)), database), "incr")["value"]
end

#cache_keys(namespace, pattern: nil, limit: nil, database: @database) ⇒ Array<Hash>

Returns one Hash per key ("key", and the TTL and size the server reports).

Parameters:

  • pattern (String, nil) (defaults to: nil) —

    a glob where * matches any run of characters

Returns:

  • (Array<Hash>) —

    one Hash per key ("key", and the TTL and size the server reports)



367
368
369
# File 'lib/tricoredb/client.rb', line 367

def cache_keys(namespace, pattern: nil, limit: nil, database: @database)
  json(cache("Keys", { "namespace" => namespace, "pattern" => pattern, "limit" => limit }, database), "keys")["keys"] || []
end

#cache_lindex(namespace, key, index, database: @database) ⇒ String?

Returns:

  • (String, nil)


404
405
406
# File 'lib/tricoredb/client.rb', line 404

def cache_lindex(namespace, key, index, database: @database)
  cache_value(cache("LIndex", nk(namespace, key).merge("index" => Integer(index)), database), "lindex")
end

#cache_llen(namespace, key, database: @database) ⇒ Integer

Returns:

  • (Integer)


399
400
401
# File 'lib/tricoredb/client.rb', line 399

def cache_llen(namespace, key, database: @database)
  json(cache("LLen", nk(namespace, key), database), "llen")["length"]
end

#cache_lpop(namespace, key, database: @database) ⇒ String?

Returns:

  • (String, nil)


382
383
384
# File 'lib/tricoredb/client.rb', line 382

def cache_lpop(namespace, key, database: @database)
  cache_value(cache("LPop", nk(namespace, key), database), "lpop")
end

#cache_lpush(namespace, key, values, database: @database) ⇒ Integer

Returns the new length.

Returns:

  • (Integer) —

    the new length



372
373
374
# File 'lib/tricoredb/client.rb', line 372

def cache_lpush(namespace, key, values, database: @database)
  json(cache("LPush", nk(namespace, key).merge("values" => byte_list(values, "values")), database), "lpush")["length"]
end

#cache_lrange(namespace, key, start, stop, database: @database) ⇒ Array<String>

Inclusive range; negative indices count from the end.

Returns:

  • (Array<String>)


393
394
395
396
# File 'lib/tricoredb/client.rb', line 393

def cache_lrange(namespace, key, start, stop, database: @database)
  body = nk(namespace, key).merge("start" => Integer(start), "stop" => Integer(stop))
  binaries(json(cache("LRange", body, database), "lrange")["values"])
end

#cache_persist(namespace, key, database: @database) ⇒ Boolean

Returns false when the key had no TTL.

Returns:

  • (Boolean) —

    false when the key had no TTL



354
355
356
# File 'lib/tricoredb/client.rb', line 354

def cache_persist(namespace, key, database: @database)
  json(cache("Persist", nk(namespace, key), database), "persist")["persisted"] == true
end

#cache_ping(database: @database) ⇒ Object

Liveness through the cache module.



307
308
309
310
# File 'lib/tricoredb/client.rb', line 307

def cache_ping(database: @database)
  request({ "Cache" => "Ping" }, database: database)
  true
end

#cache_rpop(namespace, key, database: @database) ⇒ String?

Returns:

  • (String, nil)


387
388
389
# File 'lib/tricoredb/client.rb', line 387

def cache_rpop(namespace, key, database: @database)
  cache_value(cache("RPop", nk(namespace, key), database), "rpop")
end

#cache_rpush(namespace, key, values, database: @database) ⇒ Integer

Returns the new length.

Returns:

  • (Integer) —

    the new length



377
378
379
# File 'lib/tricoredb/client.rb', line 377

def cache_rpush(namespace, key, values, database: @database)
  json(cache("RPush", nk(namespace, key).merge("values" => byte_list(values, "values")), database), "rpush")["length"]
end

#cache_sadd(namespace, key, members, database: @database) ⇒ Integer

Returns members newly added.

Returns:

  • (Integer) —

    members newly added



409
410
411
# File 'lib/tricoredb/client.rb', line 409

def cache_sadd(namespace, key, members, database: @database)
  json(cache("SAdd", nk(namespace, key).merge("members" => byte_list(members, "members")), database), "sadd")["added"]
end

#cache_scard(namespace, key, database: @database) ⇒ Integer

Returns:

  • (Integer)


425
426
427
# File 'lib/tricoredb/client.rb', line 425

def cache_scard(namespace, key, database: @database)
  json(cache("SCard", nk(namespace, key), database), "scard")["cardinality"]
end

#cache_set(namespace, key, value, ttl_ms: nil, database: @database) ⇒ Object



312
313
314
315
# File 'lib/tricoredb/client.rb', line 312

def cache_set(namespace, key, value, ttl_ms: nil, database: @database)
  cache("Set", { "namespace" => namespace, "key" => key, "value" => bytes(value, "value"), "ttl_ms" => ttl_ms }, database)
  nil
end

#cache_set_nx(namespace, key, value, ttl_ms: nil, database: @database) ⇒ Boolean

Set only if absent.

Returns:

  • (Boolean) —

    whether this call stored the value



360
361
362
363
# File 'lib/tricoredb/client.rb', line 360

def cache_set_nx(namespace, key, value, ttl_ms: nil, database: @database)
  body = nk(namespace, key).merge("value" => bytes(value, "value"), "ttl_ms" => ttl_ms)
  json(cache("SetNx", body, database), "setnx")["set"] == true
end

#cache_sismember?(namespace, key, member, database: @database) ⇒ Boolean

Returns:

  • (Boolean)


419
420
421
422
# File 'lib/tricoredb/client.rb', line 419

def cache_sismember?(namespace, key, member, database: @database)
  body = nk(namespace, key).merge("member" => bytes(member, "member"))
  json(cache("SIsMember", body, database), "sismember")["is_member"] == true
end

#cache_smembers(namespace, key, database: @database) ⇒ Array<String>

Returns in ascending byte order.

Returns:

  • (Array<String>) —

    in ascending byte order



430
431
432
# File 'lib/tricoredb/client.rb', line 430

def cache_smembers(namespace, key, database: @database)
  binaries(json(cache("SMembers", nk(namespace, key), database), "smembers")["members"])
end

#cache_srem(namespace, key, members, database: @database) ⇒ Integer

Returns members that were present.

Returns:

  • (Integer) —

    members that were present



414
415
416
# File 'lib/tricoredb/client.rb', line 414

def cache_srem(namespace, key, members, database: @database)
  json(cache("SRem", nk(namespace, key).merge("members" => byte_list(members, "members")), database), "srem")["removed"]
end

#cache_ttl(namespace, key, database: @database) ⇒ Integer?

Returns remaining milliseconds; nil when the key is missing or has no expiry.

Returns:

  • (Integer, nil) —

    remaining milliseconds; nil when the key is missing or has no expiry



333
334
335
336
# File 'lib/tricoredb/client.rb', line 333

def cache_ttl(namespace, key, database: @database)
  ttl = json(cache("Ttl", nk(namespace, key), database), "ttl")["ttl_ms"]
  ttl.is_a?(Integer) ? ttl : nil
end

#cache_xadd(namespace, key, fields, id: nil, database: @database) ⇒ String

Append a stream entry.

Parameters:

  • fields (Hash, Array<Array(String, String)>)
  • id (String, nil) (defaults to: nil) —

    nil or * to auto-generate

Returns:

  • (String) —

    the assigned <ms>-<seq> id



471
472
473
474
# File 'lib/tricoredb/client.rb', line 471

def cache_xadd(namespace, key, fields, id: nil, database: @database)
  body = nk(namespace, key).merge("id" => id, "fields" => pairs(fields, "fields"))
  json(cache("XAdd", body, database), "xadd")["id"]
end

#cache_xdel(namespace, key, ids, database: @database) ⇒ Integer

Returns entries deleted.

Returns:

  • (Integer) —

    entries deleted



495
496
497
# File 'lib/tricoredb/client.rb', line 495

def cache_xdel(namespace, key, ids, database: @database)
  json(cache("XDel", nk(namespace, key).merge("ids" => Array(ids).map(&:to_s)), database), "xdel")["deleted"]
end

#cache_xlen(namespace, key, database: @database) ⇒ Integer

Returns:

  • (Integer)


477
478
479
# File 'lib/tricoredb/client.rb', line 477

def cache_xlen(namespace, key, database: @database)
  json(cache("XLen", nk(namespace, key), database), "xlen")["length"]
end

#cache_xrange(namespace, key, start = "-", stop = "+", count: nil, database: @database) ⇒ Array<StreamEntry>

Returns oldest first.

Returns:



482
483
484
485
# File 'lib/tricoredb/client.rb', line 482

def cache_xrange(namespace, key, start = "-", stop = "+", count: nil, database: @database)
  body = nk(namespace, key).merge("start" => start, "end" => stop, "count" => count)
  stream_entries(json(cache("XRange", body, database), "xrange"))
end

#cache_xread(namespace, key, after = "0-0", count: nil, database: @database) ⇒ Array<StreamEntry>

Entries strictly newer than after. Never blocks.

Returns:



489
490
491
492
# File 'lib/tricoredb/client.rb', line 489

def cache_xread(namespace, key, after = "0-0", count: nil, database: @database)
  body = nk(namespace, key).merge("after" => after, "count" => count)
  stream_entries(json(cache("XRead", body, database), "xread"))
end

#cache_xtrim(namespace, key, max_len, database: @database) ⇒ Integer

Returns entries evicted.

Returns:

  • (Integer) —

    entries evicted



500
501
502
# File 'lib/tricoredb/client.rb', line 500

def cache_xtrim(namespace, key, max_len, database: @database)
  json(cache("XTrim", nk(namespace, key).merge("max_len" => Integer(max_len)), database), "xtrim")["trimmed"]
end

#cancel(request_id) ⇒ Integer

Ask the server to stop one of this principal's running statements.

Send this on a second connection: the connection running the statement is waiting for its reply and cannot carry anything else.

Parameters:

  • request_id (String)

Returns:

  • (Integer) —

    how many executions were cancelled (0 for an unknown id)

Raises:



169
170
171
172
173
174
# File 'lib/tricoredb/client.rb', line 169

def cancel(request_id)
  tag, body = exchange(Frame::CANCEL, { "request_id" => request_id.to_s })
  raise ServerError.new(error_text(body), code: error_code(body), frame: true) if tag == Frame::ERROR
  fail_stream(ProtocolError.new("expected CANCEL_OK, got #{Frame.name(tag)}")) unless tag == Frame::CANCEL_OK
  body.is_a?(Hash) ? Integer(body["cancelled"] || 0) : 0
end

#close ⇒ Object

Say goodbye and close the socket. Idempotent; never raises.



177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/tricoredb/client.rb', line 177

def close
  return if closed?

  begin
    exchange(Frame::CLOSE, nil, timeout: CLOSE_TIMEOUT)
  rescue StandardError
    nil
  ensure
    @transport.close
  end
  nil
end

#closed? ⇒ Boolean

Returns:

  • (Boolean)


107
108
109
# File 'lib/tricoredb/client.rb', line 107

def closed?
  @transport.closed?
end

#commit(database: @database) ⇒ Hash

Commit the open block.

Returns:

  • (Hash)


266
267
268
# File 'lib/tricoredb/client.rb', line 266

def commit(database: @database)
  txn_control("COMMIT", database)
end

#doc_aggregate(collection, pipeline, database: @database) ⇒ Array<Hash>

Parameters:

  • pipeline (Array<Hash>) —

    from Stage

Returns:

  • (Array<Hash>)


584
585
586
# File 'lib/tricoredb/client.rb', line 584

def doc_aggregate(collection, pipeline, database: @database)
  documents(document("Aggregate", { "collection" => collection, "pipeline" => pipeline }, database), "aggregate")
end

#doc_analyze(collection, database: @database) ⇒ Hash

Returns "document_count" and the other statistics.

Returns:

  • (Hash) —

    "document_count" and the other statistics



578
579
580
# File 'lib/tricoredb/client.rb', line 578

def doc_analyze(collection, database: @database)
  json(document("Analyze", { "collection" => collection }, database), "analyze")
end

#doc_create_collection(collection, database: @database) ⇒ Object

---- document --------------------------------------------------------------



506
507
508
509
# File 'lib/tricoredb/client.rb', line 506

def doc_create_collection(collection, database: @database)
  document("CreateCollection", { "collection" => collection }, database)
  nil
end

#doc_create_index(collection, index_name, field, unique: false, database: @database) ⇒ Object



561
562
563
564
565
# File 'lib/tricoredb/client.rb', line 561

def doc_create_index(collection, index_name, field, unique: false, database: @database)
  body = { "collection" => collection, "index_name" => index_name, "field" => field, "unique" => unique ? true : false }
  document("CreateIndex", body, database)
  nil
end

#doc_delete(collection, id, database: @database) ⇒ Object



546
547
548
549
# File 'lib/tricoredb/client.rb', line 546

def doc_delete(collection, id, database: @database)
  document("Delete", { "collection" => collection, "id" => id }, database)
  nil
end

#doc_drop_collection(collection, database: @database) ⇒ Object



556
557
558
559
# File 'lib/tricoredb/client.rb', line 556

def doc_drop_collection(collection, database: @database)
  document("DropCollection", { "collection" => collection }, database)
  nil
end

#doc_drop_index(collection, index_name, database: @database) ⇒ Object



567
568
569
570
# File 'lib/tricoredb/client.rb', line 567

def doc_drop_index(collection, index_name, database: @database)
  document("DropIndex", { "collection" => collection, "index_name" => index_name }, database)
  nil
end

#doc_find(collection, filter = Filter.all, limit: nil, database: @database) ⇒ Array<Hash>

Parameters:

  • filter (String, Hash) (defaults to: Filter.all) —

    from Filter

Returns:

  • (Array<Hash>)


524
525
526
# File 'lib/tricoredb/client.rb', line 524

def doc_find(collection, filter = Filter.all, limit: nil, database: @database)
  documents(document("Find", { "collection" => collection, "filter" => filter, "limit" => limit }, database), "find")
end

#doc_get(collection, id, database: @database) ⇒ Hash?

Returns:

  • (Hash, nil)


518
519
520
# File 'lib/tricoredb/client.rb', line 518

def doc_get(collection, id, database: @database)
  documents(document("Get", { "collection" => collection, "id" => id }, database), "get").first
end

#doc_insert(collection, doc, id: nil, database: @database) ⇒ String

Returns the stored id.

Parameters:

  • id (String, nil) (defaults to: nil) —

    omitted, the server generates one

Returns:

  • (String) —

    the stored id



513
514
515
# File 'lib/tricoredb/client.rb', line 513

def doc_insert(collection, doc, id: nil, database: @database)
  json(document("Insert", { "collection" => collection, "id" => id, "document" => doc }, database), "insert")["id"]
end

#doc_list_collections(database: @database) ⇒ Array<String>

Returns:

  • (Array<String>)


552
553
554
# File 'lib/tricoredb/client.rb', line 552

def doc_list_collections(database: @database)
  json(request({ "Document" => "ListCollections" }, database: database), "listCollections")["collections"] || []
end

#doc_list_indexes(collection, database: @database) ⇒ Array<Hash>

Returns "index_name", "field", "unique".

Returns:

  • (Array<Hash>) —

    "index_name", "field", "unique"



573
574
575
# File 'lib/tricoredb/client.rb', line 573

def doc_list_indexes(collection, database: @database)
  json(document("ListIndexes", { "collection" => collection }, database), "listIndexes")["indexes"] || []
end

#doc_update(collection, id, set, database: @database) ⇒ Object

Set fields on an existing document (dot paths). Not an upsert.



529
530
531
532
# File 'lib/tricoredb/client.rb', line 529

def doc_update(collection, id, set, database: @database)
  document("Update", { "collection" => collection, "id" => id, "set" => set }, database)
  nil
end

#doc_update_many(collection, filter, set: nil, inc: nil, database: @database) ⇒ Hash

Returns "matched", "modified".

Returns:

  • (Hash) —

    "matched", "modified"



541
542
543
544
# File 'lib/tricoredb/client.rb', line 541

def doc_update_many(collection, filter, set: nil, inc: nil, database: @database)
  body = { "collection" => collection, "filter" => filter, "update" => update_body(set, inc) }
  json(document("UpdateMany", body, database), "updateMany")
end

#doc_update_one(collection, id, set: nil, inc: nil, upsert: false, database: @database) ⇒ Hash

Returns "updated", "inserted", "id".

Returns:

  • (Hash) —

    "updated", "inserted", "id"



535
536
537
538
# File 'lib/tricoredb/client.rb', line 535

def doc_update_one(collection, id, set: nil, inc: nil, upsert: false, database: @database)
  body = { "collection" => collection, "id" => id, "update" => update_body(set, inc), "upsert" => upsert ? true : false }
  json(document("UpdateOne", body, database), "updateOne")
end

#exchange(tag, payload, timeout: @read_timeout) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Holds the connection lock for one frame pair. Any failure between writing and finishing the read closes the connection: the stream position is unknown.



788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
# File 'lib/tricoredb/client.rb', line 788

def exchange(tag, payload, timeout: @read_timeout)
  @lock.synchronize do
    raise ConnectionError, "connection is closed" if closed?

    bytes = Frame.encode(tag, payload)
    done = false
    begin
      @transport.io.write(bytes)
      result = @transport.read_frame(timeout)
      done = true
      result
    rescue IOError, SystemCallError, OpenSSL::SSL::SSLError => e
      raise ConnectionError, "socket error: #{e.message}"
    ensure
      unless done
        @transport.close
        @txn_open = false
      end
    end
  end
end

#execute(sql, params = nil, database: @database) ⇒ Response

Run a write or DDL statement.

Parameters:

  • sql (String)
  • params (Array, nil) (defaults to: nil) —

    values for ? placeholders, bound by the server; see Params

Returns:

Raises:



235
236
237
# File 'lib/tricoredb/client.rb', line 235

def execute(sql, params = nil, database: @database)
  request({ "Sql" => { "Exec" => sql_body(sql, params) } }, database: database)
end

#features ⇒ Hash{Symbol => Object}

Returns the granted features by name, plus :mask.

Returns:

  • (Hash{Symbol => Object}) —

    the granted features by name, plus :mask



96
97
98
99
100
101
102
103
104
# File 'lib/tricoredb/client.rb', line 96

def features
  g = @granted_features
  {
    mask: g,
    correlation_id: g & Features::CORRELATION_ID != 0,
    server_params: g & Features::SERVER_PARAMS != 0,
    session_txn: g & Features::SESSION_TXN != 0
  }
end

#graph_add_edge(graph, id, from, to, label, properties: {}, database: @database) ⇒ String

Returns the id.

Returns:

  • (String) —

    the id



678
679
680
681
# File 'lib/tricoredb/client.rb', line 678

def graph_add_edge(graph, id, from, to, label, properties: {}, database: @database)
  body = { "graph" => graph, "id" => id, "from" => from, "to" => to, "label" => label, "properties" => properties || {} }
  json(graph_op("AddEdge", body, database), "addEdge")["id"]
end

#graph_add_node(graph, id, labels: [], properties: {}, database: @database) ⇒ String

Returns the id.

Returns:

  • (String) —

    the id



662
663
664
665
# File 'lib/tricoredb/client.rb', line 662

def graph_add_node(graph, id, labels: [], properties: {}, database: @database)
  body = { "graph" => graph, "id" => id, "labels" => Array(labels), "properties" => properties || {} }
  json(graph_op("AddNode", body, database), "addNode")["id"]
end

#graph_create(graph, database: @database) ⇒ Object

---- graph -----------------------------------------------------------------

direction is outgoing (the default), incoming or both.



646
647
648
649
# File 'lib/tricoredb/client.rb', line 646

def graph_create(graph, database: @database)
  graph_op("CreateGraph", { "graph" => graph }, database)
  nil
end

#graph_degree(graph, node_id, direction: "outgoing", database: @database) ⇒ Integer

Returns:

  • (Integer)


700
701
702
703
# File 'lib/tricoredb/client.rb', line 700

def graph_degree(graph, node_id, direction: "outgoing", database: @database)
  body = { "graph" => graph, "node_id" => node_id, "direction" => direction.to_s }
  json(graph_op("Degree", body, database), "degree")["degree"]
end

#graph_delete_edge(graph, id, database: @database) ⇒ Object



688
689
690
691
# File 'lib/tricoredb/client.rb', line 688

def graph_delete_edge(graph, id, database: @database)
  graph_op("DeleteEdge", { "graph" => graph, "id" => id }, database)
  nil
end

#graph_delete_node(graph, id, database: @database) ⇒ Object



672
673
674
675
# File 'lib/tricoredb/client.rb', line 672

def graph_delete_node(graph, id, database: @database)
  graph_op("DeleteNode", { "graph" => graph, "id" => id }, database)
  nil
end

#graph_drop(graph, database: @database) ⇒ Object



651
652
653
654
# File 'lib/tricoredb/client.rb', line 651

def graph_drop(graph, database: @database)
  graph_op("DropGraph", { "graph" => graph }, database)
  nil
end

#graph_get_edge(graph, id, database: @database) ⇒ Hash?

Returns "id", "from", "to", "label", "properties".

Returns:

  • (Hash, nil) —

    "id", "from", "to", "label", "properties"



684
685
686
# File 'lib/tricoredb/client.rb', line 684

def graph_get_edge(graph, id, database: @database)
  json(graph_op("GetEdge", { "graph" => graph, "id" => id }, database), "getEdge")
end

#graph_get_node(graph, id, database: @database) ⇒ Hash?

Returns "id", "labels", "properties".

Returns:

  • (Hash, nil) —

    "id", "labels", "properties"



668
669
670
# File 'lib/tricoredb/client.rb', line 668

def graph_get_node(graph, id, database: @database)
  json(graph_op("GetNode", { "graph" => graph, "id" => id }, database), "getNode")
end

#graph_list(database: @database) ⇒ Array<String>

Returns:

  • (Array<String>)


657
658
659
# File 'lib/tricoredb/client.rb', line 657

def graph_list(database: @database)
  json(request({ "Graph" => "ListGraphs" }, database: database), "listGraphs")["graphs"] || []
end

#graph_list_edges(graph, limit: nil, offset: nil, database: @database) ⇒ Hash

Returns "edges", "count", "total", "truncated".

Returns:

  • (Hash) —

    "edges", "count", "total", "truncated"



736
737
738
# File 'lib/tricoredb/client.rb', line 736

def graph_list_edges(graph, limit: nil, offset: nil, database: @database)
  json(graph_op("ListEdges", { "graph" => graph, "limit" => limit, "offset" => offset }, database), "listEdges")
end

#graph_list_nodes(graph, limit: nil, offset: nil, database: @database) ⇒ Hash

Returns "nodes", "count", "total", "truncated".

Returns:

  • (Hash) —

    "nodes", "count", "total", "truncated"



731
732
733
# File 'lib/tricoredb/client.rb', line 731

def graph_list_nodes(graph, limit: nil, offset: nil, database: @database)
  json(graph_op("ListNodes", { "graph" => graph, "limit" => limit, "offset" => offset }, database), "listNodes")
end

#graph_neighbors(graph, node_id, direction: "outgoing", label: nil, limit: nil, database: @database) ⇒ Array<Hash>

Returns "node_id", "edge_id", "label", "direction".

Returns:

  • (Array<Hash>) —

    "node_id", "edge_id", "label", "direction"



694
695
696
697
# File 'lib/tricoredb/client.rb', line 694

def graph_neighbors(graph, node_id, direction: "outgoing", label: nil, limit: nil, database: @database)
  body = { "graph" => graph, "node_id" => node_id, "direction" => direction.to_s, "label" => label, "limit" => limit }
  json(graph_op("Neighbors", body, database), "neighbors")["neighbors"] || []
end

#graph_query(graph, cypher, database: @database) ⇒ Hash

Read-only Cypher subset.

Returns:

  • (Hash) —

    "columns", "rows", "count", "truncated"



742
743
744
# File 'lib/tricoredb/client.rb', line 742

def graph_query(graph, cypher, database: @database)
  json(graph_op("Query", { "graph" => graph, "cypher" => cypher }, database), "query")
end

#graph_shortest_path(graph, from, to, direction: "outgoing", label: nil, max_depth: nil, database: @database) ⇒ Hash

Fewest hops. "No path" is "found" => false, not an error.

Returns:

  • (Hash) —

    "found", "hops", "node_path", "edge_path"



715
716
717
718
719
# File 'lib/tricoredb/client.rb', line 715

def graph_shortest_path(graph, from, to, direction: "outgoing", label: nil, max_depth: nil, database: @database)
  body = { "graph" => graph, "from" => from, "to" => to, "direction" => direction.to_s, "label" => label,
           "max_depth" => max_depth }
  json(graph_op("ShortestPath", body, database), "shortestPath")
end

#graph_traverse(graph, start, direction: "outgoing", label: nil, max_depth: nil, limit: nil, database: @database) ⇒ Hash

Bounded breadth-first walk.

Returns:

  • (Hash) —

    "nodes" (each with "depth"), "count", "truncated", "max_depth", "limit"



707
708
709
710
711
# File 'lib/tricoredb/client.rb', line 707

def graph_traverse(graph, start, direction: "outgoing", label: nil, max_depth: nil, limit: nil, database: @database)
  body = { "graph" => graph, "start" => start, "direction" => direction.to_s, "label" => label,
           "max_depth" => max_depth, "limit" => limit }
  json(graph_op("Traverse", body, database), "traverse")
end

#graph_weighted_shortest_path(graph, from, to, direction: "outgoing", label: nil, weight_property: nil, database: @database) ⇒ Hash

Least summed edge weight.

Returns:

  • (Hash) —

    "found", "total_cost", "node_path", "edge_path"



723
724
725
726
727
728
# File 'lib/tricoredb/client.rb', line 723

def graph_weighted_shortest_path(graph, from, to, direction: "outgoing", label: nil, weight_property: nil,
                                 database: @database)
  body = { "graph" => graph, "from" => from, "to" => to, "direction" => direction.to_s, "label" => label,
           "weight_property" => weight_property }
  json(graph_op("WeightedShortestPath", body, database), "weightedShortestPath")
end

#handshake(client_name: "tricoredb-ruby/#{VERSION}", features: Features::ALL, timeout: 10) ⇒ Integer

Send HELLO and read HELLO_OK. Called by connect.

Returns:

  • (Integer) —

    the granted feature bitmap



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/tricoredb/client.rb', line 118

def handshake(client_name: "tricoredb-ruby/#{VERSION}", features: Features::ALL, timeout: 10)
  tag, body = exchange(Frame::HELLO, {
                         "protocol" => "tricore",
                         "version" => { "major" => 1, "minor" => 0 },
                         "client" => client_name,
                         "features" => features
                       }, timeout: timeout)
  if tag == Frame::ERROR
    fail_stream(ProtocolError.new(error_text(body), code: error_code(body)))
  end
  fail_stream(ProtocolError.new("expected HELLO_OK, got #{Frame.name(tag)}")) unless tag == Frame::HELLO_OK
  unless body.is_a?(Hash) && body["ok"] == true
    message = body.is_a?(Hash) ? body["message"] : nil
    fail_stream(ProtocolError.new(message || "handshake refused", code: body.is_a?(Hash) ? body["code"] : nil))
  end
  @granted_features = Integer(body["features"] || 0)
end

#in_transaction? ⇒ Boolean

Returns true from a successful #begin_transaction until commit or rollback.

Returns:



112
113
114
# File 'lib/tricoredb/client.rb', line 112

def in_transaction?
  @txn_open && !closed?
end

#llm_context(sources, format: "toon", max_rows: nil, redact_sensitive: true, include_schema: false, database: @database) ⇒ String, Object

Assemble a context bundle from read-only sources.

Parameters:

  • sources (Array<Hash>) —

    from LlmSource, or {sql:} / {collection:}

Returns:

  • (String, Object)

Raises:

  • (ArgumentError)


759
760
761
762
763
764
765
766
767
# File 'lib/tricoredb/client.rb', line 759

def llm_context(sources, format: "toon", max_rows: nil, redact_sensitive: true, include_schema: false,
                database: @database)
  wire = (sources.is_a?(Array) ? sources : [sources]).map { |s| LlmSource.coerce(s) }
  raise ArgumentError, "a context bundle needs at least one source" if wire.empty?

  body = { "sources" => wire, "format" => format.to_s,
           "options" => llm_options(max_rows, redact_sensitive, include_schema) }
  rendered(request({ "Llm" => { "Context" => body } }, database: database))
end

#llm_schema(format: "toon", max_rows: nil, redact_sensitive: true, include_schema: false, database: @database) ⇒ String, Object

Export the schema catalogue.

Parameters:

  • format (String) (defaults to: "toon") —

    toon, json, markdown or native

Returns:

  • (String, Object) —

    text for toon/markdown, a JSON value otherwise



751
752
753
754
# File 'lib/tricoredb/client.rb', line 751

def llm_schema(format: "toon", max_rows: nil, redact_sensitive: true, include_schema: false, database: @database)
  body = { "format" => format.to_s, "options" => llm_options(max_rows, redact_sensitive, include_schema) }
  rendered(request({ "Llm" => { "Schema" => body } }, database: database))
end

#ping ⇒ true

Exchange PING/PONG. Never reaches a module; see #admin_ping for a full round trip.

Returns:

  • (true)


156
157
158
159
160
# File 'lib/tricoredb/client.rb', line 156

def ping
  tag, _body = exchange(Frame::PING, nil)
  fail_stream(ProtocolError.new("expected PONG, got #{Frame.name(tag)}")) unless tag == Frame::PONG
  true
end

#query(sql, params = nil, database: @database) ⇒ Rows

Run a read. The server refuses a write sent this way.

Parameters:

  • sql (String)
  • params (Array, nil) (defaults to: nil)

Returns:

Raises:



244
245
246
247
248
249
250
# File 'lib/tricoredb/client.rb', line 244

def query(sql, params = nil, database: @database)
  resp = request({ "Sql" => { "Query" => sql_body(sql, params) } }, database: database)
  rows = resp.arm("Rows")
  raise ProtocolError, "expected Rows, got #{resp.kind}" unless rows.is_a?(Hash)

  Rows.new(rows["columns"] || [], rows["rows"] || [])
end

#request(op, database: @database, correlation_id: nil) ⇒ Response

Send a raw operation.

Parameters:

  • op (Hash, String) —

    an externally tagged TriCoreOp, e.g. {"Cache" => "Ping"}

  • database (String) (defaults to: @database)
  • correlation_id (String, nil) (defaults to: nil) —

    requires the CORRELATION_ID feature

Returns:

  • (Response) —

    only when its status is ok

Raises:

  • (ServerError) —

    for any other status, or an ERROR frame



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/tricoredb/client.rb', line 197

def request(op, database: @database, correlation_id: nil)
  payload = {
    "request_id" => next_request_id,
    "database" => database,
    "region_hint" => nil,
    "op" => op
  }
  unless correlation_id.nil?
    require_feature(Features::CORRELATION_ID, "CORRELATION_ID", "a correlation id")
    payload["correlation_id"] = correlation_id.to_s
  end
  unless @request_timeout_ms.nil?
    payload["options"] = {
      "cache" => { "mode" => "disabled" },
      "output" => "native",
      "consistency" => "strong_primary",
      "timeout_ms" => Integer(@request_timeout_ms),
      "llm" => nil
    }
  end
  tag, body = exchange(Frame::REQUEST, payload)
  raise ServerError.new(error_text(body), code: error_code(body), frame: true) if tag == Frame::ERROR
  fail_stream(ProtocolError.new("expected RESPONSE, got #{Frame.name(tag)}")) unless tag == Frame::RESPONSE

  response = Response.new(body)
  raise response.to_error(@txn_open) unless response.ok?

  response
end

#rollback(database: @database) ⇒ Hash

Discard the open block.

Returns:

  • (Hash)


272
273
274
# File 'lib/tricoredb/client.rb', line 272

def rollback(database: @database)
  txn_control("ROLLBACK", database)
end

#transaction(database: @database) {|db| ... } ⇒ Object

Begin, yield this connection, and commit. If the block raises, roll back and re-raise the original exception.

Yield Parameters:

Returns:

  • (Object) —

    the block's value

Raises:

  • (ArgumentError)


281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/tricoredb/client.rb', line 281

def transaction(database: @database)
  raise ArgumentError, "transaction needs a block" unless block_given?

  begin_transaction(database: database)
  begin
    result = yield self
  rescue Exception # rubocop:disable Lint/RescueException
    if in_transaction?
      begin
        rollback(database: database)
      rescue StandardError
        nil
      end
    end
    raise
  end
  commit(database: database)
  result
end

#vector_create_collection(collection, dimension, metric: "cosine", quantization: "none", database: @database) ⇒ Object

Parameters:

  • metric (String) (defaults to: "cosine") —

    cosine, dot or l2

  • quantization (String) (defaults to: "none") —

    none or int8



592
593
594
595
596
597
# File 'lib/tricoredb/client.rb', line 592

def vector_create_collection(collection, dimension, metric: "cosine", quantization: "none", database: @database)
  body = { "collection" => collection, "dimension" => Integer(dimension), "metric" => metric.to_s,
           "quantization" => quantization.to_s }
  vector("CreateCollection", body, database)
  nil
end

#vector_delete(collection, id, database: @database) ⇒ Object



610
611
612
613
# File 'lib/tricoredb/client.rb', line 610

def vector_delete(collection, id, database: @database)
  vector("Delete", { "collection" => collection, "id" => id }, database)
  nil
end

#vector_describe_collection(collection, database: @database) ⇒ Hash

Returns "dimension", "metric", "count", "quantization".

Returns:

  • (Hash) —

    "dimension", "metric", "count", "quantization"



628
629
630
# File 'lib/tricoredb/client.rb', line 628

def vector_describe_collection(collection, database: @database)
  json(vector("DescribeCollection", { "collection" => collection }, database), "describeCollection")
end

#vector_drop_collection(collection, database: @database) ⇒ Object



637
638
639
640
# File 'lib/tricoredb/client.rb', line 637

def vector_drop_collection(collection, database: @database)
  vector("DropCollection", { "collection" => collection }, database)
  nil
end

#vector_get(collection, id, database: @database) ⇒ Hash?

Returns "id", "vector", "metadata".

Returns:

  • (Hash, nil) —

    "id", "vector", "metadata"



606
607
608
# File 'lib/tricoredb/client.rb', line 606

def vector_get(collection, id, database: @database)
  json(vector("Get", { "collection" => collection, "id" => id }, database), "vector get")
end

#vector_list_collections(database: @database) ⇒ Array<String>

Returns:

  • (Array<String>)


623
624
625
# File 'lib/tricoredb/client.rb', line 623

def vector_list_collections(database: @database)
  json(request({ "Vector" => "ListCollections" }, database: database), "listCollections")["collections"] || []
end

#vector_list_vectors(collection, limit: nil, offset: nil, database: @database) ⇒ Hash

Returns "vectors", "count", "total", "truncated".

Returns:

  • (Hash) —

    "vectors", "count", "total", "truncated"



633
634
635
# File 'lib/tricoredb/client.rb', line 633

def vector_list_vectors(collection, limit: nil, offset: nil, database: @database)
  json(vector("ListVectors", { "collection" => collection, "limit" => limit, "offset" => offset }, database), "listVectors")
end

#vector_search(collection, values, top_k, filter: nil, database: @database) ⇒ Array<Hash>

Returns "id", "score", "metadata", best first.

Parameters:

  • filter (Hash, nil) (defaults to: nil) —

    metadata field => required value (exact equality)

Returns:

  • (Array<Hash>) —

    "id", "score", "metadata", best first



617
618
619
620
# File 'lib/tricoredb/client.rb', line 617

def vector_search(collection, values, top_k, filter: nil, database: @database)
  body = { "collection" => collection, "vector" => numbers(values), "top_k" => Integer(top_k), "filter" => filter }
  json(vector("Search", body, database), "search")["results"] || []
end

#vector_upsert(collection, id, values, metadata: nil, database: @database) ⇒ String

Returns the id.

Returns:

  • (String) —

    the id



600
601
602
603
# File 'lib/tricoredb/client.rb', line 600

def vector_upsert(collection, id, values, metadata: nil, database: @database)
  body = { "collection" => collection, "id" => id, "vector" => numbers(values), "metadata" =>  }
  json(vector("Upsert", body, database), "upsert")["id"]
end