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