ytsaurus_api/lib.rs
1//! The transport-independent YTsaurus client interface.
2//!
3//! YTsaurus reaches its dynamic tables two ways — HTTP API v4 and the RPC proxy
4//! — and the C++ client does not make callers choose an API to go with the
5//! transport. It has one interface and two constructors:
6//!
7//! ```cpp
8//! IClientPtr CreateClient (const TString& serverName, ...); // HTTP
9//! IClientPtr CreateRpcClient(const TString& serverName, ...); // RPC proxy
10//! ```
11//!
12//! This crate is the Rust equivalent of the interface those return, and the
13//! layering mirrors the C++ exactly:
14//!
15//! | C++ | here |
16//! | --- | --- |
17//! | `yt/yt/client/api` — the interface | this crate |
18//! | `yt/yt/client/api/rpc_proxy` — one implementation | `ytsaurus-rpc` |
19//! | `yt/cpp/mapreduce` — the wrapper with both constructors | `ytsaurus-client` |
20//!
21//! So the two constructors live in `ytsaurus-client`, which depends on both,
22//! and switching transport is one line:
23//!
24//! ```ignore
25//! let client = ytsaurus_client::create_client("localhost:8000")?; // HTTP
26//! let client = ytsaurus_client::create_rpc_client("localhost:8011")?; // RPC
27//! // everything below is identical
28//! let rows = client.lookup_rows("//tmp/t", &[key], &LookupOptions::default())?;
29//! ```
30//!
31//! # This interface is synchronous
32//!
33//! Deliberately, and it is the one decision here worth arguing about. The C++
34//! wrapper is blocking, every other crate in this workspace is synchronous, and
35//! a MapReduce job is a synchronous, single-purpose process. So the shared
36//! interface blocks.
37//!
38//! `ytsaurus-rpc`'s own API stays `async`, and callers who want multiplexed
39//! in-flight requests — the entire reason the RPC proxy exists — should use it
40//! directly rather than through this. What this buys is portability between
41//! transports, not concurrency.
42//!
43//! # What it covers
44//!
45//! The dynamic-table surface both transports implement: reads, writes and the
46//! transactions they run in. Cypress, operations and file I/O stay on
47//! `ytsaurus-client`, because the RPC crate deliberately does not implement
48//! them and an interface with half its methods unavailable on one transport
49//! would be worse than two honest APIs.
50
51pub mod error;
52pub mod value;
53
54pub use error::{Error, Result};
55pub use value::{MaybeRow, Row, Value};
56
57/// A read timestamp.
58pub type Timestamp = u64;
59
60/// Options for [`TableClient::lookup_rows`].
61#[derive(Debug, Clone, Default)]
62pub struct LookupOptions {
63 /// The columns to return. Empty means all of them.
64 pub columns: Vec<String>,
65 /// The timestamp to read at. `None` reads the latest committed data.
66 ///
67 /// Inside a transaction this is filled in for you; setting it by hand there
68 /// would read at a different point than the transaction sees.
69 pub timestamp: Option<Timestamp>,
70}
71
72/// Options for [`TableClient::select_rows`].
73#[derive(Debug, Clone, Default)]
74pub struct SelectOptions {
75 /// The timestamp to read at. `None` reads the latest committed data.
76 pub timestamp: Option<Timestamp>,
77 /// Stop after this many rows, if set.
78 pub limit: Option<u64>,
79}
80
81/// What a dynamic table can be asked to do, whatever the transport.
82///
83/// Implemented by the HTTP client and by the RPC client's blocking facade. The
84/// two are wire-level different and behaviourally the same, which is the whole
85/// point.
86pub trait TableClient {
87 /// Which transport this client speaks, for diagnostics and for tests that
88 /// want to run the same checks against both.
89 fn transport(&self) -> Transport;
90
91 /// Looks rows up by key.
92 ///
93 /// `keys` holds one row per key, carrying only the key columns. The result
94 /// has **one entry per key, in the order asked**, and a key with no row
95 /// comes back as `None`.
96 fn lookup_rows(
97 &self,
98 path: &str,
99 keys: &[Row],
100 options: &LookupOptions,
101 ) -> Result<Vec<MaybeRow>>;
102
103 /// Runs a query and returns its rows.
104 fn select_rows(&self, query: &str, options: &SelectOptions) -> Result<Vec<Row>>;
105
106 /// Writes rows outside a transaction.
107 ///
108 /// The transports differ underneath — HTTP has a standalone `insert_rows`
109 /// command, RPC has none and needs a transaction — and this hides that.
110 fn insert_rows(&self, path: &str, rows: &[Row]) -> Result<()>;
111
112 /// Deletes rows by key, outside a transaction.
113 fn delete_rows(&self, path: &str, keys: &[Row]) -> Result<()>;
114
115 /// Starts a tablet transaction.
116 ///
117 /// Boxed because the transaction types differ per transport and a caller
118 /// holding a `dyn TableClient` cannot name either.
119 fn start_transaction(&self) -> Result<Box<dyn TableTransaction + '_>>;
120}
121
122/// A transaction over a dynamic table.
123///
124/// Dropping one does **not** abort it: neither transport can abort reliably
125/// from `Drop`, and a silent best-effort attempt would be a lie. An unfinished
126/// transaction expires on the server after its timeout.
127pub trait TableTransaction {
128 /// The transaction id, as the cluster shows it.
129 fn id(&self) -> String;
130
131 /// Looks rows up as of this transaction.
132 fn lookup_rows(
133 &self,
134 path: &str,
135 keys: &[Row],
136 options: &LookupOptions,
137 ) -> Result<Vec<MaybeRow>>;
138
139 /// Runs a query as of this transaction.
140 fn select_rows(&self, query: &str, options: &SelectOptions) -> Result<Vec<Row>>;
141
142 /// Writes rows in this transaction.
143 fn insert_rows(&self, path: &str, rows: &[Row]) -> Result<()>;
144
145 /// Deletes rows by key in this transaction.
146 fn delete_rows(&self, path: &str, keys: &[Row]) -> Result<()>;
147
148 /// Tells the server the transaction is still wanted.
149 fn ping(&self) -> Result<()>;
150
151 /// Commits. Takes the transaction by box because it consumes it.
152 fn commit(self: Box<Self>) -> Result<()>;
153
154 /// Aborts.
155 fn abort(self: Box<Self>) -> Result<()>;
156}
157
158/// Which wire a client speaks.
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum Transport {
161 /// HTTP API v4.
162 Http,
163 /// The RPC proxy, over bus.
164 Rpc,
165}
166
167impl std::fmt::Display for Transport {
168 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169 formatter.write_str(match self {
170 Self::Http => "HTTP",
171 Self::Rpc => "RPC",
172 })
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 /// The interface has to be usable behind a `dyn`, or the two constructors
181 /// cannot return the same thing — which is the entire point of it.
182 #[test]
183 fn the_interface_is_object_safe() {
184 struct Nothing;
185
186 impl TableClient for Nothing {
187 fn transport(&self) -> Transport {
188 Transport::Http
189 }
190 fn lookup_rows(&self, _: &str, _: &[Row], _: &LookupOptions) -> Result<Vec<MaybeRow>> {
191 Ok(Vec::new())
192 }
193 fn select_rows(&self, _: &str, _: &SelectOptions) -> Result<Vec<Row>> {
194 Ok(Vec::new())
195 }
196 fn insert_rows(&self, _: &str, _: &[Row]) -> Result<()> {
197 Ok(())
198 }
199 fn delete_rows(&self, _: &str, _: &[Row]) -> Result<()> {
200 Ok(())
201 }
202 fn start_transaction(&self) -> Result<Box<dyn TableTransaction + '_>> {
203 Err(Error::Unsupported {
204 transport: Transport::Http,
205 what: "transactions in this stub",
206 })
207 }
208 }
209
210 let client: Box<dyn TableClient> = Box::new(Nothing);
211 assert_eq!(client.transport(), Transport::Http);
212 assert!(
213 client
214 .lookup_rows("//tmp/t", &[], &LookupOptions::default())
215 .is_ok()
216 );
217 }
218
219 #[test]
220 fn transport_names_itself() {
221 assert_eq!(Transport::Http.to_string(), "HTTP");
222 assert_eq!(Transport::Rpc.to_string(), "RPC");
223 }
224}