Skip to main content

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}