1mod handshake;
15mod io;
16#[cfg(feature = "redwire")]
17pub mod scram;
18#[cfg(feature = "redwire-tls")]
19mod tls;
20
21pub use reddb_wire::redwire::{Flags, Frame, FrameError, MessageKind};
22#[cfg(feature = "redwire-tls")]
23pub use tls::TlsConfig;
24
25use std::io as std_io;
26use std::pin::Pin;
27
28use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
29use tokio::net::TcpStream;
30
31use reddb_wire::query_with_params::{
32 encode_query_with_params, ParamValue as RedWireParamValue, FEATURE_PARAMS,
33};
34
35pub(crate) type Stream = Pin<Box<dyn AsyncReadWrite + Send + Unpin>>;
39
40pub(crate) trait AsyncReadWrite: AsyncRead + AsyncWrite {}
41impl<T: AsyncRead + AsyncWrite + ?Sized> AsyncReadWrite for T {}
42
43use crate::error::{ClientError, ErrorCode, Result};
44use crate::types::{BulkInsertResult, QueryResult};
45
46use handshake::HandshakeOutcome;
47use reddb_wire::legacy::WireValue;
48use reddb_wire::redwire::{
49 build_bulk_insert_binary_frame, build_bulk_insert_frame, build_bye_frame, build_delete_frame,
50 build_get_frame, build_ping_frame, build_query_frame, build_query_with_params_frame,
51 decode_bulk_ok_count_payload, decode_bulk_ok_payload, decode_delete_ok_affected,
52 decode_get_result_payload, decode_query_result_payload, decode_text_payload,
53 encode_bulk_binary_payload, encode_bulk_insert_payload, encode_insert_payload,
54 encode_key_payload, expect_bulk_ok_or_error, expect_delete_ok_or_error, expect_pong_reply,
55 expect_result_or_error, supported_client_preface, BuildError, OperationReplyError,
56};
57
58#[derive(Clone)]
60pub enum Auth {
61 Anonymous,
63 Bearer(String),
65 Basic { user: String, pass: String },
67 ApiKey(String),
69}
70
71impl std::fmt::Debug for Auth {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 match self {
74 Self::Anonymous => f.write_str("Anonymous"),
75 Self::Bearer(_) => f.debug_tuple("Bearer").field(&"<redacted>").finish(),
76 Self::Basic { .. } => f
77 .debug_struct("Basic")
78 .field("user", &"<redacted>")
79 .field("pass", &"<redacted>")
80 .finish(),
81 Self::ApiKey(_) => f.debug_tuple("ApiKey").field(&"<redacted>").finish(),
82 }
83 }
84}
85
86#[derive(Debug, Clone)]
89pub enum BinaryValue {
90 I64(i64),
91 F64(f64),
92 Text(String),
93 Bool(bool),
94 Null,
95}
96
97impl From<&BinaryValue> for WireValue {
98 fn from(value: &BinaryValue) -> Self {
99 match value {
100 BinaryValue::I64(n) => WireValue::I64(*n),
101 BinaryValue::F64(n) => WireValue::F64(*n),
102 BinaryValue::Text(value) => WireValue::Text(value.clone()),
103 BinaryValue::Bool(value) => WireValue::Bool(*value),
104 BinaryValue::Null => WireValue::Null,
105 }
106 }
107}
108
109pub struct ConnectOptions {
111 pub host: String,
112 pub port: u16,
113 pub auth: Auth,
114 pub client_name: Option<String>,
115 #[cfg(feature = "redwire-tls")]
116 pub tls: Option<TlsConfig>,
117}
118
119impl std::fmt::Debug for ConnectOptions {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 let mut s = f.debug_struct("ConnectOptions");
122 s.field("host", &self.host)
123 .field("port", &self.port)
124 .field("auth", &self.auth)
125 .field("client_name", &self.client_name);
126 #[cfg(feature = "redwire-tls")]
127 s.field("tls", &self.tls.is_some());
128 s.finish()
129 }
130}
131
132impl Clone for ConnectOptions {
133 fn clone(&self) -> Self {
134 Self {
135 host: self.host.clone(),
136 port: self.port,
137 auth: self.auth.clone(),
138 client_name: self.client_name.clone(),
139 #[cfg(feature = "redwire-tls")]
140 tls: self.tls.clone(),
141 }
142 }
143}
144
145impl ConnectOptions {
146 pub fn new(host: impl Into<String>, port: u16) -> Self {
147 Self {
148 host: host.into(),
149 port,
150 auth: Auth::Anonymous,
151 client_name: Some(format!("reddb-rs/{}", env!("CARGO_PKG_VERSION"))),
152 #[cfg(feature = "redwire-tls")]
153 tls: None,
154 }
155 }
156
157 pub fn with_auth(mut self, auth: Auth) -> Self {
158 self.auth = auth;
159 self
160 }
161
162 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
163 self.client_name = Some(name.into());
164 self
165 }
166
167 #[cfg(feature = "redwire-tls")]
169 pub fn with_tls(mut self, tls: TlsConfig) -> Self {
170 self.tls = Some(tls);
171 self
172 }
173}
174
175pub struct RedWireClient {
176 stream: Stream,
177 next_correlation_id: u64,
178 #[allow(dead_code)]
179 session_id: String,
180 #[allow(dead_code)]
181 server_features: u32,
182}
183
184impl std::fmt::Debug for RedWireClient {
185 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 f.debug_struct("RedWireClient")
187 .field("session_id", &self.session_id)
188 .field("server_features", &self.server_features)
189 .finish()
190 }
191}
192
193impl RedWireClient {
194 pub async fn connect(opts: ConnectOptions) -> Result<Self> {
195 let addr = format!("{}:{}", opts.host, opts.port);
196 let tcp = TcpStream::connect(&addr)
197 .await
198 .map_err(|e| ClientError::new(ErrorCode::Network, format!("{addr}: {e}")))?;
199
200 let mut stream: Stream = match () {
201 #[cfg(feature = "redwire-tls")]
202 _ if opts.tls.is_some() => {
203 let tls_cfg = opts.tls.as_ref().unwrap();
204 let tls_stream = tls::wrap_client(tcp, &opts.host, tls_cfg).await?;
205 Box::pin(tls_stream)
206 }
207 _ => Box::pin(tcp),
208 };
209
210 stream
212 .write_all(&supported_client_preface())
213 .await
214 .map_err(io_err)?;
215
216 let outcome = handshake::run(&mut stream, &opts).await?;
217 match outcome {
218 HandshakeOutcome::Authenticated {
219 session_id,
220 server_features,
221 } => Ok(Self {
222 stream,
223 next_correlation_id: 1,
224 session_id,
225 server_features,
226 }),
227 HandshakeOutcome::Refused(reason) => Err(ClientError::new(
228 ErrorCode::AuthRefused,
229 format!("redwire auth refused: {reason}"),
230 )),
231 }
232 }
233
234 pub async fn query(&mut self, sql: &str) -> Result<QueryResult> {
235 let raw = self.query_raw(sql).await?;
236 let value = decode_query_result_payload(raw.as_bytes())
237 .map_err(|e| ClientError::new(ErrorCode::Protocol, format!("decode result: {e}")))?;
238 Ok(QueryResult::from_envelope(value))
239 }
240
241 pub async fn query_raw(&mut self, sql: &str) -> Result<String> {
245 let corr = self.next_corr();
246 let req = build_query_frame(corr, sql).map_err(frame_build_err)?;
247 io::write_frame(&mut self.stream, &req).await?;
248 let resp = self.read_frame().await?;
249 let payload = expect_result_or_error(resp.kind, &resp.payload).map_err(reply_err)?;
250 Ok(decode_text_payload(payload))
251 }
252
253 pub async fn query_with(
254 &mut self,
255 sql: &str,
256 params: &[crate::params::Value],
257 ) -> Result<QueryResult> {
258 if params.is_empty() {
259 return self.query(sql).await;
260 }
261 if self.server_features & FEATURE_PARAMS == 0 {
262 return Err(ClientError::new(
263 ErrorCode::ParamsUnsupported,
264 "server did not advertise RedWire parameter support",
265 ));
266 }
267
268 let wire_params = params.iter().map(param_to_redwire).collect::<Vec<_>>();
269 let payload = encode_query_with_params(sql, &wire_params)
270 .map_err(|e| ClientError::new(ErrorCode::Protocol, format!("encode params: {e}")))?;
271 let corr = self.next_corr();
272 let req = build_query_with_params_frame(corr, payload).map_err(frame_build_err)?;
273 io::write_frame(&mut self.stream, &req).await?;
274 let resp = self.read_frame().await?;
275 let payload = expect_result_or_error(resp.kind, &resp.payload).map_err(reply_err)?;
276 let value = decode_query_result_payload(payload)
277 .map_err(|e| ClientError::new(ErrorCode::Protocol, format!("decode result: {e}")))?;
278 Ok(QueryResult::from_envelope(value))
279 }
280
281 pub async fn insert(&mut self, collection: &str, payload: serde_json::Value) -> Result<u64> {
284 self.send_insert_frame(encode_insert_payload(collection, payload))
285 .await
286 .map(|result| result.affected)
287 }
288
289 pub async fn bulk_insert(
291 &mut self,
292 collection: &str,
293 payloads: Vec<serde_json::Value>,
294 ) -> Result<BulkInsertResult> {
295 self.send_insert_frame(encode_bulk_insert_payload(collection, payloads))
296 .await
297 }
298
299 async fn send_insert_frame(&mut self, bytes: Vec<u8>) -> Result<BulkInsertResult> {
300 let corr = self.next_corr();
301 let req = build_bulk_insert_frame(corr, bytes).map_err(frame_build_err)?;
302 io::write_frame(&mut self.stream, &req).await?;
303 let resp = self.read_frame().await?;
304 let payload = expect_bulk_ok_or_error(resp.kind, &resp.payload).map_err(reply_err)?;
305 let payload = decode_bulk_ok_payload(payload)
306 .map_err(|e| ClientError::new(ErrorCode::Protocol, format!("decode bulk_ok: {e}")))?;
307 Ok(BulkInsertResult {
308 affected: payload.affected,
309 rids: payload.rids,
310 ids: payload.ids,
311 })
312 }
313
314 pub async fn get(&mut self, collection: &str, id: &str) -> Result<serde_json::Value> {
317 let corr = self.next_corr();
318 let req =
319 build_get_frame(corr, encode_key_payload(collection, id)).map_err(frame_build_err)?;
320 io::write_frame(&mut self.stream, &req).await?;
321 let resp = self.read_frame().await?;
322 let payload = expect_result_or_error(resp.kind, &resp.payload).map_err(reply_err)?;
323 decode_get_result_payload(payload)
324 .map_err(|e| ClientError::new(ErrorCode::Protocol, format!("decode get: {e}")))
325 }
326
327 pub async fn delete(&mut self, collection: &str, id: &str) -> Result<u64> {
329 let corr = self.next_corr();
330 let req = build_delete_frame(corr, encode_key_payload(collection, id))
331 .map_err(frame_build_err)?;
332 io::write_frame(&mut self.stream, &req).await?;
333 let resp = self.read_frame().await?;
334 let payload = expect_delete_ok_or_error(resp.kind, &resp.payload).map_err(reply_err)?;
335 decode_delete_ok_affected(payload)
336 .map_err(|e| ClientError::new(ErrorCode::Protocol, format!("decode delete_ok: {e}")))
337 }
338
339 pub async fn bulk_insert_binary(
348 &mut self,
349 collection: &str,
350 columns: &[&str],
351 rows: &[Vec<BinaryValue>],
352 ) -> Result<u64> {
353 let wire_rows = rows
354 .iter()
355 .map(|row| row.iter().map(WireValue::from).collect::<Vec<_>>())
356 .collect::<Vec<_>>();
357 let payload = encode_bulk_binary_payload(collection, columns, &wire_rows)
358 .map_err(|e| ClientError::new(ErrorCode::Protocol, e.to_string()))?;
359
360 let corr = self.next_corr();
361 let req = build_bulk_insert_binary_frame(corr, payload).map_err(frame_build_err)?;
362 io::write_frame(&mut self.stream, &req).await?;
363 let resp = self.read_frame().await?;
364 let payload = expect_bulk_ok_or_error(resp.kind, &resp.payload).map_err(reply_err)?;
365 decode_bulk_ok_count_payload(payload)
366 .map_err(|e| ClientError::new(ErrorCode::Protocol, e.to_string()))
367 }
368
369 pub async fn ping(&mut self) -> Result<()> {
370 let corr = self.next_corr();
371 let req = build_ping_frame(corr).map_err(frame_build_err)?;
372 io::write_frame(&mut self.stream, &req).await?;
373 let resp = self.read_frame().await?;
374 expect_pong_reply(resp.kind).map_err(reply_err)
375 }
376
377 pub async fn close(mut self) -> Result<()> {
378 let corr = self.next_corr();
379 let bye = build_bye_frame(corr).map_err(frame_build_err)?;
380 let _ = io::write_frame(&mut self.stream, &bye).await;
381 Ok(())
382 }
383
384 fn next_corr(&mut self) -> u64 {
385 let c = self.next_correlation_id;
386 self.next_correlation_id = self.next_correlation_id.wrapping_add(1);
387 c
388 }
389
390 async fn read_frame(&mut self) -> Result<Frame> {
391 io::read_frame(&mut self.stream).await
392 }
393}
394
395fn param_to_redwire(value: &crate::params::Value) -> RedWireParamValue {
396 match value {
397 crate::params::Value::Null => RedWireParamValue::Null,
398 crate::params::Value::Bool(value) => RedWireParamValue::Bool(*value),
399 crate::params::Value::Int(value) => RedWireParamValue::Int(*value),
400 crate::params::Value::Float(value) => RedWireParamValue::Float(*value),
401 crate::params::Value::Text(value) => RedWireParamValue::Text(value.clone()),
402 crate::params::Value::Bytes(value) => RedWireParamValue::Bytes(value.clone()),
403 crate::params::Value::Vector(value) => RedWireParamValue::Vector(value.clone()),
404 crate::params::Value::Json(value) => {
405 RedWireParamValue::Json(value.to_json_string().into_bytes())
406 }
407 crate::params::Value::Timestamp(value) => RedWireParamValue::Timestamp(*value),
408 crate::params::Value::Uuid(value) => RedWireParamValue::Uuid(*value),
409 }
410}
411
412fn io_err(err: std_io::Error) -> ClientError {
413 ClientError::new(ErrorCode::Network, err.to_string())
414}
415
416fn frame_build_err(err: BuildError) -> ClientError {
417 ClientError::new(ErrorCode::Protocol, format!("build redwire frame: {err}"))
418}
419
420fn reply_err(err: OperationReplyError) -> ClientError {
421 match err {
422 OperationReplyError::Engine(message) => ClientError::new(ErrorCode::Engine, message),
423 OperationReplyError::UnexpectedKind { .. } => {
424 ClientError::new(ErrorCode::Protocol, err.to_string())
425 }
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::param_to_redwire;
432 use crate::{JsonValue, Value};
433 use reddb_wire::query_with_params::ParamValue as WireValue;
434
435 #[test]
436 fn param_to_redwire_preserves_all_wire_variants() {
437 let uuid = [0x11; 16];
438 let cases = vec![
439 (Value::Null, WireValue::Null),
440 (Value::Bool(true), WireValue::Bool(true)),
441 (Value::Int64(42), WireValue::Int(42)),
442 (Value::Float(1.5), WireValue::Float(1.5)),
443 (Value::Text("Ada".into()), WireValue::Text("Ada".into())),
444 (
445 Value::Bytes(vec![0xde, 0xad]),
446 WireValue::Bytes(vec![0xde, 0xad]),
447 ),
448 (
449 Value::Vector(vec![0.25, 0.5]),
450 WireValue::Vector(vec![0.25, 0.5]),
451 ),
452 (
453 Value::Json(JsonValue::object([("role", JsonValue::string("admin"))])),
454 WireValue::Json(br#"{"role":"admin"}"#.to_vec()),
455 ),
456 (
457 Value::Timestamp(1_700_000_000),
458 WireValue::Timestamp(1_700_000_000),
459 ),
460 (Value::Uuid(uuid), WireValue::Uuid(uuid)),
461 ];
462
463 for (input, expected) in cases {
464 assert_eq!(param_to_redwire(&input), expected);
465 }
466 }
467}