tricoredb
Official Rust client for TriCoreDB: SQL, documents, vectors, graphs and cache over one native connection.
- Blocking, no async runtime. Calls block; a
Poolserves concurrent work. - Server-side parameters. Values never become part of the SQL text.
- Typed errors you branch on by kind and code, never by message.
- Transactions, connection pooling, TLS and mutual TLS.
- Four dependencies, two of them optional:
serde,serde_json, andrustlsonly when TLS is on.
Contents
- Requirements
- Installation
- Running a server
- Quick start
- Connecting
- SQL
- Transactions
- Connection pool
- Cache
- Documents
- Vectors
- Graphs
- LLM context
- Admin
- Errors
- TLS
- Testing
Requirements
- Rust 1.85 or later, which is what
rustlsand its dependencies need. - A TriCoreDB server speaking protocol 1.0 (
tricore-server0.1.0-rc.1 or later). See Running a server. - The methods added in 0.3.0 — marked below — need
tricore-server0.1.0-rc.3 or later. An older server does not know those operations and answers them with an error; everything else works against it as before.
Installation
Or in Cargo.toml:
[]
= "0.3"
TLS is on by default. For a build with no cryptography dependencies at all:
[]
= { = "0.3", = false }
Running a server
The quickest way is the official Docker image,
trinesh14/tricoredb.
Local development (no TLS and no encryption, for this machine only). Set
TRICORE_ADMIN_PASSWORD in your shell first. Then create the admin and start
the server:
Anything else: by default the image runs with TLS on and an encrypted data volume. Follow the quick start on the Docker Hub page to create the certificate and key, then connect with TLS.
TRICORE_MODULES=all enables every data model. The image's default is sql,
document and cache. A call to a disabled model fails with an error whose
code() is engine.disabled.
Quick start
use ;
Connecting
Client::connect opens one authenticated connection. Dropping it ends the
session; close() says goodbye first and reports whether the socket closed
cleanly.
Options field |
Default | Meaning |
|---|---|---|
host |
"127.0.0.1" |
Server host |
port |
8427 (tricoredb::DEFAULT_PORT) |
Server port |
user |
None |
Principal to authenticate as |
secret |
None |
Password or token |
database |
"main" |
Database named in every request |
client_name |
"tricoredb-rust/<version>" |
Name reported in the handshake |
connect_timeout |
None |
Bound on the connect and TLS handshake |
read_timeout |
None |
Bound on each wait for a reply |
tls |
None (plain TCP) |
See TLS |
features |
all | Capability bitmap announced in the handshake |
follow_leader |
true |
Send a not_leader refusal again, to the leader |
leader_retries |
5 |
How many times a refused request is sent again |
leader_retry_delay |
250ms |
First pause before asking again; doubles up to 2 s |
Each field has a builder method, so options read as one expression:
use Duration;
use Options;
let options = new
.user
.secret
.database
.connect_timeout;
One connection runs one request at a time. Every method takes &mut self,
so sharing a Client between threads does not compile — the frame interleaving
that would corrupt a connection is ruled out rather than documented. Use a
pool for concurrency.
SQL
query runs only SELECT. execute runs everything else. The server enforces
the split: a write sent through query is refused.
#
execute_params and query_params bind ? placeholders on the server. The
values travel next to the statement, so a value can never be read as SQL syntax,
however it is spelled. The params! macro builds the list from mixed types:
| Rust value | Sent as |
|---|---|
None |
SQL NULL |
bool |
a boolean |
any integer, up to i128 / u128 |
an exact number, never through f64 |
f32 / f64 |
a number; NaN and infinities are refused |
Param::decimal("10.50") |
exact digits, no exponent — for DECIMAL |
Vec<u8> / &[u8] |
0x-prefixed hex, for BLOB |
&str / String |
text |
Binding needs the SERVER_PARAMS capability, agreed in the handshake
(db.server_params_granted()). If the server did not grant it, these calls fail
with ErrorKind::FeatureNotGranted before anything is sent. They never fall
back to pasting values into the statement text: escaping and binding are not the
same guarantee.
Transactions
Two kinds, and they are not interchangeable.
transaction sends a whole BEGIN … COMMIT script in one request. It works
on every node, and is right when every statement is known up front:
#
begin, commit and rollback keep a transaction open across requests on
this connection, so a later statement can depend on what an earlier one read.
with_transaction commits when the closure returns Ok and rolls back on
Err:
#
Session transactions need the SESSION_TXN capability
(db.session_txn_granted()). Without it, begin fails by name rather than
silently running each statement on its own. The transaction belongs to this
connection: another connection cannot commit it, and a dropped socket rolls it
back.
Connection pool
A Pool is built from the same Options, so every pooled connection uses the
same TLS settings. It is Clone and Send, so threads share one pool.
#
with_connection lends a connection for the duration of a closure rather than
handing back a guard, because a guard is something a caller can hold across
threads. A connection is never returned to the pool with a transaction still
open: it is rolled back first, and retired if that rollback fails.
Cache
Values are bytes. None is a miss, which is how a miss is told apart from a
stored empty value.
#
| Family | Methods |
|---|---|
| Keys | cache_get, cache_set, cache_set_ttl, cache_set_nx, cache_delete, cache_exists, cache_ttl, cache_expire, cache_persist, cache_incr, cache_keys, cache_clear_namespace, cache_ping |
| Lists | cache_lpush, cache_rpush, cache_lpop, cache_rpop, cache_lrange, cache_llen, cache_lindex |
| Sets | cache_sadd, cache_srem, cache_sismember, cache_scard, cache_smembers |
| Hashes | cache_hset, cache_hset_text, cache_hget, cache_hdel, cache_hgetall, cache_hexists, cache_hlen |
| Streams | cache_xadd, cache_xadd_text, cache_xlen, cache_xrange, cache_xread, cache_xdel, cache_xtrim |
Added in 0.3.0, for server 0.1.0-rc.3 and later:
| Family | Methods |
|---|---|
| Many keys, conditions | cache_mget, cache_mset, cache_warm, cache_set_if, cache_compare_and_swap, cache_get_del, cache_get_set, cache_get_ex, cache_append, cache_expire_at |
| Stale-while-revalidate | cache_set_fresh, cache_get_fresh, cache_set_missing |
| Sorted sets | cache_zadd, cache_zrem, cache_zscore, cache_zcard, cache_zrank, cache_zrange, cache_zrange_with_scores, cache_zrange_by_score, cache_zrange_by_score_with_scores, cache_zincr_by |
| Locks | cache_lock_acquire, cache_lock_renew, cache_lock_release |
| Bitmaps | cache_set_bit, cache_get_bit, cache_bit_count, cache_bit_pos, cache_bit_op |
| HyperLogLog | cache_pf_add, cache_pf_count, cache_pf_merge |
| Lists, sets, hashes | cache_lmove, cache_ltrim, cache_blpop, cache_brpop, cache_sunion, cache_sinter, cache_sdiff, cache_srand_member, cache_hincr_by |
| Pipelines and scripts | cache_pipeline, cache_transaction, cache_script_load, cache_script_run |
| Publish and poll | cache_publish, cache_poll |
| Node and database | cache_stats, cache_hot_keys, cache_purge_expired, cache_flush_database, cache_set_namespace_limit, cache_get_namespace_limit |
#
A sorted-set score is always finite, and JSON has no spelling for an infinity,
so an open range end is ScoreBound::lowest() or ScoreBound::highest(); a
non-finite score or bound is refused before anything is sent.
cache_hot_keys answers only when the server's [cache] hot_keys setting is
above zero, and returns the payload as the server sent it.
cache_flush_database deletes every cache key in the connection's database.
Documents
Filters and pipeline stages are built with constructors, so you never write the request JSON by hand.
#
Aggregation uses the same shape:
#
Also available: document_insert_with_id, document_get,
document_find_limit, document_update, document_upsert_one,
document_update_many, document_delete, document_list_collections,
document_drop_collection, document_create_index, document_drop_index,
document_list_indexes and document_analyze.
Added in 0.3.0, for server 0.1.0-rc.3 and later: document_replace,
document_insert_many, document_delete_many, document_bulk_write,
document_rename_collection, document_purge_expired, document_explain,
document_rebuild_index, document_set_collection_options,
document_get_collection_options, document_begin_transaction,
document_commit_transaction and document_rollback_transaction.
#
Vectors
Fixed-dimension collections with cosine, dot or L2 distance, metadata filters
and optional int8 quantization.
#
The score is a similarity: higher is closer under every metric, and results
come back best first. L2 is the case worth knowing — the server negates the
squared distance, so an L2 score is <= 0 and -0.02 is nearer than -196.0.
Also available: vector_create_collection_quantized, vector_search_filtered,
vector_get, vector_delete, vector_list_collections,
vector_describe_collection, vector_list_vectors and
vector_drop_collection.
Added in 0.3.0, for server 0.1.0-rc.3 and later: vector_upsert_many,
vector_upsert_text, vector_upsert_named, vector_add_named_vector,
vector_delete_many, vector_delete_by_filter, vector_update_by_filter,
vector_count, vector_search_batch, vector_search_multi,
vector_search_by_examples, vector_embed, vector_set_alias,
vector_list_aliases, vector_list_partitions, vector_rename_collection,
vector_copy_collection, vector_set_collection_config,
vector_rebuild_index, vector_index_stats and vector_measure_recall.
VectorSearchOptions carries the filter, the named vector to search (using)
and the other per-search settings; a field left at its default is not sent.
Graphs
Labelled nodes, typed edges, traversal, two shortest-path algorithms and a read-only Cypher subset.
#
"No path" comes back as found == false, not as an error, and message says
whether the search ran out of graph or stopped at a bound.
Also available: graph_get_node, graph_get_edge, graph_delete_node,
graph_delete_edge, graph_list, graph_drop, graph_traverse,
graph_weighted_shortest_path, graph_degree, graph_list_nodes,
graph_list_edges and graph_query.
Added in 0.3.0, for server 0.1.0-rc.3 and later:
| Family | Methods |
|---|---|
| Batch writes | graph_add_many, graph_remove_many, graph_update_node, graph_update_edge, graph_copy, graph_import_csv |
| Walks and paths | graph_dfs, graph_subgraph, graph_k_shortest_paths, graph_all_paths |
| Algorithms | graph_connected_components, graph_detect_cycle, graph_centrality, graph_communities, graph_triangle_count, graph_node_similarity, graph_page_rank |
| Descriptions | graph_stats, graph_schema, graph_check_consistency |
| Indexes and constraints | graph_create_node_index, graph_create_constraint, graph_drop_schema_object, graph_list_schema_objects, graph_find_nodes |
| Cypher writes | graph_execute, graph_begin_transaction, graph_commit_transaction, graph_rollback_transaction |
A graph transaction queues graph_execute statements sent while
set_request_txn names it, and the commit applies them all or none.
LLM context
Read-only export of query results and schema, in TOON, JSON, Markdown or the native format.
#
Sensitive fields are redacted by default — LlmOptions::default() matches the
server's own defaults, so build from it rather than from a zeroed struct.
Admin
#
Admin calls need the cluster module enabled on the server, even on a single
node. db.ping() checks the connection itself and reaches no module.
Errors
Every fallible call returns tricoredb::Error. Branch on kind() and code(),
never on the message text:
#
is_connection_fatal() says whether the connection can still be used: a server
refusal leaves it perfectly usable, while a timeout or a protocol failure does
not. A connection that failed that way is dropped, and every later call on it
reports the original failure instead of reading a stale reply as the next
answer.
Leader redirects. In a cluster only the leader accepts writes, and a
follower refuses with code() == "not_leader" (is_redirect()). The client
handles that itself unless follow_leader(false) is set:
- when the refusal names the leader (
leader_hint()), it opens a connection there with the same user, secret and TLS settings (the leader's host is the certificate name), sends the request again, and keeps that connection (address()reports it); - while an election is in progress (no hint), it waits and asks the same node again, pausing 250 ms, then 500 ms, up to 2 s;
- after
leader_retries(5) it returns the last refusal.
This is safe because a not_leader refusal was applied nowhere. Nothing is sent
again inside a session transaction, and never a refusal with
code() == Some(OUTCOME_UNKNOWN): a follower handed that request to the leader
and lost the answer, or the leader could not confirm it in time, so it may have
been applied. The leader's address must be reachable from your application;
when it is not, the client keeps its connection and waits.
TLS
TLS is off unless Options::tls is set. Without it the secret crosses the wire
in the clear, so use TLS for anything but local development.
#
With TLS on, the server certificate and host name are always verified. If
ca_file is not set the trust store is empty: the client never falls back
to the operating system's roots, so a wrong path fails closed instead of
trusting an unrelated CA.
For mutual TLS, TlsOptions::client_identity(cert, key) presents a client
certificate. Errors name a certificate or key file's path, never its
contents.
TlsOptions field |
Default | Meaning |
|---|---|---|
ca_file |
None (empty trust store) |
PEM bundle used to verify the server |
server_name |
"localhost" |
Expected name (SNI and certificate check) |
client_cert_file |
None |
PEM client certificate chain, for mutual TLS |
client_key_file |
None |
PEM private key for that certificate |
danger_accept_invalid_certs |
false |
Development only. Skips all verification. |
Testing
The unit tests and the scripted-peer tests in tests/protocol.rs need no
server. tests/live.rs starts a real tricore-server: point
TRICORE_SERVER_BIN at the binary to run it. Without that, those tests print
why and pass.