Skip to main content

ignite_client/
client.rs

1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3
4use crate::protocol::binary::client_ops::{
5    BinaryTypeCache, fetch_binary_type, register_binary_type,
6};
7use crate::protocol::binary::metadata::BinaryType;
8use crate::protocol::handshake::HandshakeRequest;
9use crate::protocol::messages::{
10    SqlFieldsRequest, decode_cache_names_response, decode_tx_start_response,
11    encode_cache_create_with_config, encode_cache_get_names, encode_tx_start,
12};
13use crate::protocol::{IgniteValue, StatementType, TxConcurrency, TxIsolation, cache_id, op_code};
14use crate::transport::{IgniteConnection, next_request_id};
15
16use crate::affinity::AffinityContext;
17use crate::cache::{IgniteCache, destroy_cache_by_name, get_or_create_cache_by_name};
18use crate::channel::ChannelRegistry;
19use crate::error::{IgniteError, Result};
20use crate::pool::IgniteClientConfig;
21use crate::query::{QueryResult, UpdateResult};
22use crate::stream::{self, QueryStream};
23use crate::transaction::{Transaction, execute_sql_fields, extract_rows_affected};
24
25/// The main Ignite client.  Wraps a per-node connection registry with optional
26/// partition-aware routing; cheap to clone.
27#[derive(Clone)]
28pub struct IgniteClient {
29    registry: Arc<ChannelRegistry>,
30    affinity: Arc<AffinityContext>,
31    config: Arc<IgniteClientConfig>,
32    /// Client-side cache of binary-type metadata fetched via `OP_BINARY_TYPE_GET`,
33    /// keyed by type id.  Shared across clones (all clones of an `IgniteClient`
34    /// refer to the same logical client and connection pool).
35    binary_types: Arc<BinaryTypeCache>,
36}
37
38impl IgniteClient {
39    /// Create a new client.  Opens one pool per configured node address (no
40    /// connections are made yet).  Partition awareness defaults to on when ≥ 2
41    /// nodes are configured, unless explicitly overridden in the config.
42    pub fn new(config: IgniteClientConfig) -> Self {
43        let config = Arc::new(config);
44        let registry = Arc::new(ChannelRegistry::new(config.clone()));
45        let enabled = config
46            .partition_awareness
47            .unwrap_or(registry.node_count() >= 2);
48        let affinity = Arc::new(AffinityContext::new(enabled));
49        Self {
50            registry,
51            affinity,
52            config,
53            binary_types: Arc::new(Mutex::new(HashMap::new())),
54        }
55    }
56
57    /// Execute a SELECT statement and return all rows.
58    ///
59    /// # Example
60    /// ```no_run
61    /// # use ignite_client::{IgniteClient, IgniteClientConfig, IgniteValue};
62    /// # #[tokio::main] async fn main() {
63    /// let client = IgniteClient::new(IgniteClientConfig::new("localhost:10800"));
64    /// let result = client.query(
65    ///     "SELECT id, name FROM PUBLIC.users WHERE active = ?",
66    ///     vec![IgniteValue::Bool(true)],
67    /// ).await.unwrap();
68    /// for row in &result.rows {
69    ///     println!("{:?}", row.values());
70    /// }
71    /// # }
72    /// ```
73    pub async fn query(&self, sql: &str, params: Vec<IgniteValue>) -> Result<QueryResult> {
74        let conn_obj = self.registry.get(None).await?;
75        let mut req = SqlFieldsRequest::new(sql, params);
76        req.page_size = self.config.page_size as i32;
77        execute_sql_fields(&conn_obj, req).await
78    }
79
80    /// Execute a SELECT and return rows lazily as a [`QueryStream`].
81    ///
82    /// The first page is fetched immediately; subsequent pages are fetched on
83    /// demand as the stream is polled.  Use [`Self::query`] if you need all
84    /// rows in a `Vec` up front.
85    ///
86    /// The underlying connection is borrowed from the pool for the request and
87    /// returned immediately; the stream holds a shared handle (clone) to the
88    /// same TCP connection via the multiplexing design.
89    pub async fn query_stream(&self, sql: &str, params: Vec<IgniteValue>) -> Result<QueryStream> {
90        use crate::protocol::messages::SqlFieldsFirstPage;
91        use crate::protocol::op_code;
92
93        let conn_obj = self.registry.get(None).await?;
94        // Shallow-clone shares the underlying TCP connection; the pool Object
95        // can be returned immediately (the slot becomes available again).
96        let conn = Arc::new(conn_obj.clone());
97        drop(conn_obj);
98
99        let mut req = SqlFieldsRequest::new(sql, params);
100        req.page_size = self.config.page_size as i32;
101        let req_id = next_request_id();
102        let payload = req.encode(op_code::QUERY_SQL_FIELDS, req_id);
103
104        let mut resp = conn
105            .request(req_id, payload)
106            .await
107            .map_err(IgniteError::Transport)?;
108
109        let first = SqlFieldsFirstPage::decode(&mut resp, req.include_field_names)
110            .map_err(IgniteError::Protocol)?;
111
112        Ok(stream::build_stream(conn, first))
113    }
114
115    /// Execute a DML statement (INSERT/UPDATE/DELETE).
116    #[must_use = "futures do nothing unless you `.await` them"]
117    pub async fn execute(&self, sql: &str, params: Vec<IgniteValue>) -> Result<UpdateResult> {
118        let conn_obj = self.registry.get(None).await?;
119        let req = SqlFieldsRequest {
120            statement_type: StatementType::Update,
121            ..SqlFieldsRequest::new(sql, params)
122        };
123        let result = execute_sql_fields(&conn_obj, req).await?;
124        Ok(UpdateResult {
125            rows_affected: extract_rows_affected(&result),
126        })
127    }
128
129    /// Begin a new transaction with Pessimistic / ReadCommitted isolation (sensible default).
130    pub async fn begin_transaction(&self) -> Result<Transaction> {
131        self.begin_transaction_with(TxConcurrency::Pessimistic, TxIsolation::ReadCommitted, 0)
132            .await
133    }
134
135    /// Begin a transaction with explicit concurrency/isolation settings.
136    ///
137    /// Opens a **dedicated** TCP connection for the transaction's lifetime so
138    /// that the connection pool is not held hostage.  The connection is closed
139    /// when the Transaction is dropped.
140    pub async fn begin_transaction_with(
141        &self,
142        concurrency: TxConcurrency,
143        isolation: TxIsolation,
144        timeout_ms: i64,
145    ) -> Result<Transaction> {
146        // Open a dedicated connection for this transaction
147        let hs = HandshakeRequest::new(self.config.username.clone(), self.config.password.clone());
148        let tls = if self.config.use_tls {
149            Some(
150                crate::transport::build_tls_config(self.config.tls_accept_invalid_certs)
151                    .map_err(IgniteError::Transport)?,
152            )
153        } else {
154            None
155        };
156        let conn = IgniteConnection::connect(
157            &self.config.address,
158            hs,
159            Some(self.config.connect_timeout),
160            Some(self.config.request_timeout),
161            tls,
162        )
163        .await
164        .map_err(IgniteError::Transport)?;
165
166        let req_id = next_request_id();
167        let payload = encode_tx_start(
168            op_code::TX_START,
169            req_id,
170            concurrency,
171            isolation,
172            timeout_ms,
173            None,
174        );
175
176        let mut response = conn
177            .request(req_id, payload)
178            .await
179            .map_err(IgniteError::Transport)?;
180
181        let tx_id = decode_tx_start_response(&mut response).map_err(IgniteError::Protocol)?;
182
183        Ok(Transaction::new(
184            tx_id,
185            Arc::new(conn),
186            self.config.page_size as i32,
187        ))
188    }
189
190    /// Convenience: run a closure in a transaction, committing on success.
191    /// The closure receives the transaction and must return it alongside its result.
192    pub async fn with_transaction<F, Fut, T>(&self, f: F) -> Result<T>
193    where
194        F: FnOnce(Transaction) -> Fut,
195        Fut: std::future::Future<Output = Result<(Transaction, T)>>,
196    {
197        let tx = self.begin_transaction().await?;
198        match f(tx).await {
199            Ok((tx, result)) => {
200                tx.commit().await?;
201                Ok(result)
202            }
203            Err(e) => Err(e),
204        }
205    }
206
207    /// Pool status for observability.
208    pub fn pool_status(&self) -> deadpool::managed::Status {
209        self.registry.primary_status()
210    }
211
212    // ── KV cache API ──────────────────────────────────────────────────────────
213
214    /// Return a [`IgniteCache`] handle for a cache that is assumed to already
215    /// exist.  This is a pure in-process operation (no network round-trip).
216    pub fn cache(&self, name: &str) -> IgniteCache {
217        IgniteCache::new(
218            cache_id(name),
219            self.registry.clone(),
220            self.affinity.clone(),
221            self.binary_types.clone(),
222        )
223    }
224
225    /// Create the named cache if it does not already exist, then return a
226    /// handle to it.  Equivalent to `CACHE_GET_OR_CREATE_WITH_NAME`.
227    pub async fn get_or_create_cache(&self, name: &str) -> Result<IgniteCache> {
228        get_or_create_cache_by_name(name, &self.registry, &self.affinity, &self.binary_types).await
229    }
230
231    /// Create the named cache with **TRANSACTIONAL** atomicity if it does not already exist,
232    /// then return a handle to it.  Uses `CACHE_GET_OR_CREATE_WITH_CONFIGURATION` (op 1054).
233    ///
234    /// Required for caches that will be used inside KV transactions on Ignite ≥ 2.16, which
235    /// forbids atomic-cache operations inside transactions.
236    pub async fn get_or_create_transactional_cache(&self, name: &str) -> Result<IgniteCache> {
237        let req_id = next_request_id();
238        let payload = encode_cache_create_with_config(
239            op_code::CACHE_GET_OR_CREATE_WITH_CONFIGURATION,
240            req_id,
241            name,
242            true, // transactional = true
243        );
244        let conn = self.registry.get(None).await?;
245        conn.request(req_id, payload)
246            .await
247            .map_err(IgniteError::Transport)?;
248        // Response body is void — success means the cache exists with TRANSACTIONAL atomicity.
249        Ok(IgniteCache::new(
250            cache_id(name),
251            self.registry.clone(),
252            self.affinity.clone(),
253            self.binary_types.clone(),
254        ))
255    }
256
257    /// Destroy the named cache.  All data is permanently lost.
258    pub async fn destroy_cache(&self, name: &str) -> Result<()> {
259        destroy_cache_by_name(name, &self.registry).await
260    }
261
262    /// Return the names of all caches currently defined on the server.
263    pub async fn cache_names(&self) -> Result<Vec<String>> {
264        let req_id = next_request_id();
265        let payload = encode_cache_get_names(op_code::CACHE_GET_NAMES, req_id);
266        let conn = self.registry.get(None).await?;
267        let mut resp = conn
268            .request(req_id, payload)
269            .await
270            .map_err(IgniteError::Transport)?;
271        decode_cache_names_response(&mut resp).map_err(IgniteError::Protocol)
272    }
273
274    // ── Binary-type metadata (compact-footer support) ──────────────────────────
275
276    /// Return the binary-type metadata for `type_id`, needed to decode
277    /// compact-footer binary objects (schema field ids aren't recoverable
278    /// without it).
279    ///
280    /// Checks a client-side cache first; on a miss, fetches it from the
281    /// cluster via `OP_BINARY_TYPE_GET` and caches the result for subsequent
282    /// calls. Returns `Ok(None)` if the server has no metadata registered for
283    /// `type_id` (e.g. nothing of that type has ever been written).
284    pub async fn binary_type(&self, type_id: i32) -> Result<Option<Arc<BinaryType>>> {
285        fetch_binary_type(&self.registry, &self.binary_types, type_id).await
286    }
287
288    /// Register a binary type's metadata with the cluster via
289    /// `OP_BINARY_TYPE_PUT`, then cache an `Arc` clone under `t.type_id`.
290    ///
291    /// This lets Rust register a brand-new type the Java side has never seen
292    /// (or re-register an existing one — the server accepts idempotent PUTs
293    /// of identical metadata). The success response body is empty; the
294    /// connection's `request()` already validates the header and returns an
295    /// error for a failure response, so reaching this point means the PUT
296    /// succeeded.
297    pub async fn register_binary_type(&self, t: &BinaryType) -> Result<()> {
298        register_binary_type(&self.registry, &self.binary_types, t).await
299    }
300}